Compare commits

..

No commits in common. "873bc64cd25aa4b342b8e6d873f1af970aac66cc" and "7680034ba697c25b07d9db11c712364afe934958" have entirely different histories.

29 changed files with 5911 additions and 2573 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,18 +0,0 @@
@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;
}

View File

@ -1,112 +0,0 @@
<template>
<div class="flex flex-col gap-4 h-full">
<!-- Header Stats -->
<div class="flex justify-between items-center bg-[#231533] p-3 border-2 border-[#4a3b5e]">
<div class="flex gap-4">
<span class="text-[#99e550] tracking-widest uppercase">Unlocked: {{ unlocked }}/{{ total }}</span>
</div>
<div class="w-1/3 flex items-center gap-2">
<span class="text-[#99e550] tracking-widest uppercase text-sm whitespace-nowrap">Progress: {{ percentage }}%</span>
<RetroProgressBar :progress="percentage" color="#99e550" />
</div>
</div>
<div class="bg-[#0f0816] p-2 border-l-4 border-[#99e550] mb-2">
<h3 class="text-[#9fd75b] text-lg font-bold flex items-center gap-2">
<span class="text-xl"></span> ACHIEVEMENT LIST ({{ unlocked }}/{{ total }})
</h3>
</div>
<!-- Grid Layout for Achievements -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 overflow-y-auto pb-4">
<div
v-for="achievement in achievements"
:key="achievement.id"
class="relative border-2 p-3 flex flex-col gap-2 min-h-[140px] transition-all group hover:bg-[#231533]"
:style="{ borderColor: achievement.unlocked ? '#99e550' : '#4a3b5e', backgroundColor: achievement.unlocked ? '#150c1f' : '#0f0816' }"
>
<!-- Header: Icon + Title -->
<div class="flex items-start gap-3">
<div class="p-2 rounded-sm border-2" :class="achievement.unlocked ? 'border-[#99e550] bg-[#4b692f]/20' : 'border-[#4a3b5e] bg-[#2b193f]'">
<component :is="ICON_MAP[achievement.icon] || Trophy" :size="24" :color="achievement.unlocked ? achievement.color || '#ffe762' : '#8f80a0'" />
</div>
<div class="flex flex-col">
<h4 class="font-bold tracking-wide leading-none mb-1" :class="achievement.unlocked ? 'text-[#2ce8f4]' : 'text-[#8f80a0]'">
{{ achievement.title }}
</h4>
<span v-if="achievement.unlocked" class="text-[10px] text-[#99e550] uppercase tracking-widest">Completed</span>
<span v-else class="text-[10px] text-[#8f80a0] uppercase tracking-widest flex items-center gap-1">
<Lock :size="10" /> Locked
</span>
</div>
</div>
<!-- Description -->
<p class="text-xs text-[#e0d8f0] flex-grow leading-tight">
{{ achievement.description }}
</p>
<!-- Reward Section (if exists) -->
<div v-if="achievement.reward" class="text-[10px] text-[#99e550]">
<span class="text-[#99e550] opacity-70">Reward: </span>
{{ achievement.reward }}
</div>
<!-- Progress Bar (if incomplete) -->
<div v-if="!achievement.unlocked && achievement.maxValue" class="mt-auto">
<div class="flex justify-between text-[9px] text-[#8f80a0] mb-0.5">
<span>{{ achievement.currentValue }} / {{ achievement.maxValue }}</span>
<span>{{ achievement.progress }}%</span>
</div>
<div class="h-1 bg-[#2b193f] w-full">
<div class="h-full bg-[#4a3b5e]" :style="{ width: `${achievement.progress}%` }" />
</div>
</div>
<!-- Decorative corner if unlocked -->
<div v-if="achievement.unlocked" class="absolute top-0 right-0 w-4 h-4 overflow-hidden">
<div class="absolute top-0 right-0 w-2 h-2 bg-[#99e550]" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { CheckCircle2, Lock, Trophy, Baby, CalendarDays, Egg, Sprout, Cake, Star, Diamond, Milk, Utensils, Gamepad2, Sparkles, BookOpen, Search, Leaf, Dumbbell, Brush, Pill } from 'lucide-vue-next';
import RetroProgressBar from './RetroProgressBar.vue'; // Need to create this one too!
import type { Achievement } from '~/types/pixel';
interface Props {
achievements: Achievement[];
}
const props = defineProps<Props>();
const ICON_MAP: Record<string, any> = {
baby: Baby,
calendar: CalendarDays,
egg: Egg,
sprout: Sprout,
cake: Cake,
star: Star,
diamond: Diamond,
milk: Milk,
utensils: Utensils,
gamepad: Gamepad2,
sparkles: Sparkles,
book: BookOpen,
search: Search,
leaf: Leaf,
dumbbell: Dumbbell,
brush: Brush,
pill: Pill,
trophy: Trophy,
};
const total = computed(() => props.achievements.length);
const unlocked = computed(() => props.achievements.filter(a => a.unlocked).length);
const percentage = computed(() => Math.round((unlocked.value / total.value) * 100));
</script>

View File

@ -1,146 +0,0 @@
<template>
<div class="h-full flex flex-col p-4 gap-4 bg-[#1b1026] overflow-y-auto custom-scrollbar">
<!-- Table Background styling -->
<div class="absolute inset-0 bg-[#231533] opacity-50 pointer-events-none" />
<!-- Main Grid Layout -->
<div class="flex-grow grid grid-cols-12 gap-2 z-10">
<!-- Left: Hand (Col 3) -->
<div class="col-span-3 flex flex-col gap-2">
<PixelFrame title="HAND" class="h-full bg-[#1b1026]" variant="inset">
<div class="flex flex-col gap-2 h-full overflow-y-auto pr-1 custom-scrollbar">
<div v-for="(card, i) in handCards" :key="i" class="bg-[#2b193f] border-2 border-[#4a3b5e] p-1.5 flex flex-col hover:-translate-y-1 transition-transform cursor-pointer group shadow-lg">
<div class="flex justify-between items-start mb-1">
<div class="w-5 h-5 bg-[#1b1026] flex items-center justify-center rounded-sm">
<span class="text-[#f6b26b] text-[10px] font-bold">{{ card.cost }}</span>
</div>
<component :is="card.icon" :size="14" :color="card.color" />
</div>
<span class="text-xs text-[#e0d8f0] uppercase tracking-wide group-hover:text-[#f6b26b]">{{ card.name }}</span>
</div>
<!-- Empty Slot -->
<div class="border-2 border-dashed border-[#4a3b5e] rounded h-16 opacity-30 flex items-center justify-center text-xs">EMPTY</div>
</div>
</PixelFrame>
</div>
<!-- Center: Action Grid (Col 6) -->
<div class="col-span-6 flex flex-col relative">
<PixelFrame class="h-full bg-[#2b193f]">
<div class="grid grid-cols-4 grid-rows-3 gap-2 h-full p-1">
<template v-for="(action, index) in gridItems" :key="index">
<button
v-if="action"
@click="handleActionClick(action.id)"
class="relative bg-[#1b1026] border-2 border-[#4a3b5e] hover:border-[#f6b26b] hover:bg-[#231533] active:bg-[#f6b26b] active:border-[#f6b26b] group flex flex-col items-center justify-center p-1 transition-colors"
>
<div class="mb-1 p-1 rounded-sm bg-[#231533] group-active:bg-[#1b1026]">
<component :is="action.icon" :size="20" :color="action.color" class="group-active:text-[#f6b26b]" />
</div>
<span class="text-[9px] md:text-[10px] text-[#8f80a0] uppercase tracking-wider group-hover:text-white group-active:text-[#1b1026] font-bold text-center leading-tight">
{{ action.label }}
</span>
<!-- Corner deco -->
<div class="absolute top-0 right-0 w-1 h-1 bg-[#4a3b5e] group-hover:bg-[#f6b26b]" />
<div class="absolute bottom-0 left-0 w-1 h-1 bg-[#4a3b5e] group-hover:bg-[#f6b26b]" />
</button>
<div v-else class="bg-[#150c1f] border-2 border-[#2b193f] flex items-center justify-center opacity-50 cursor-not-allowed">
<div class="w-2 h-2 bg-[#2b193f] rounded-full"></div>
</div>
</template>
</div>
</PixelFrame>
</div>
<!-- Right: Stats & EQ (Col 3) -->
<div class="col-span-3 flex flex-col gap-2">
<!-- Stats Table -->
<PixelFrame title="STATS" class="bg-[#1b1026] h-2/3" variant="inset">
<div class="grid grid-cols-2 gap-x-2 content-start h-full p-1 overflow-y-auto custom-scrollbar">
<div v-for="(stat, i) in statsList" :key="i" class="flex justify-between items-center border-b border-[#2b193f] pb-0.5 mb-0.5">
<span class="text-[#8f80a0] text-[9px]">{{ stat.l }}</span>
<span class="font-mono text-[10px]" :style="{ color: stat.c || '#e0d8f0' }">{{ stat.v }}</span>
</div>
</div>
</PixelFrame>
<!-- Equipment Grid -->
<PixelFrame title="EQ" class="bg-[#1b1026] h-1/3" variant="inset">
<div
class="grid grid-cols-4 gap-1 h-full content-center cursor-pointer relative group"
@click="$emit('openInventory')"
title="Open Backpack"
>
<div v-for="(Icon, idx) in [Crown, Shirt, Hand, Footprints]" :key="idx" class="aspect-square bg-[#2b193f] border border-[#4a3b5e] flex items-center justify-center group-hover:border-[#f6b26b] transition-colors">
<component :is="Icon" :size="12" class="text-[#5a4b6e] group-hover:text-[#e0d8f0]" />
</div>
<!-- Hover Hint -->
<div class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 flex items-center justify-center text-[10px] text-[#f6b26b] font-bold pointer-events-none">
OPEN
</div>
</div>
</PixelFrame>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import PixelFrame from './PixelFrame.vue';
import {
Sword, Shield, FlaskConical, Crown, Hand, Footprints, Shirt,
Utensils, Gamepad2, Dumbbell, Puzzle, Brush, Pill, Sun, Sparkles, ShoppingBag, Swords
} from 'lucide-vue-next';
import type { EntityStats } from '~/types/pixel';
interface Props {
playerStats?: EntityStats;
}
const props = defineProps<Props>();
const emit = defineEmits(['openInventory', 'openGodSystem', 'openShop', 'openAdventure']);
const ACTIONS = [
{ id: 'feed', icon: Utensils, color: '#9fd75b', label: 'FEED 餵食' },
{ id: 'play', icon: Gamepad2, color: '#f6b26b', label: 'PLAY 玩耍' },
{ id: 'train', icon: Dumbbell, color: '#d75b5b', label: 'TRAIN 訓練' },
{ id: 'puzzle', icon: Puzzle, color: '#2ce8f4', label: 'PUZZLE 益智' },
{ id: 'clean', icon: Brush, color: '#8f80a0', label: 'CLEAN 清理' },
{ id: 'heal', icon: Pill, color: '#9fd75b', label: 'HEAL 治療' },
{ id: 'fight', icon: Swords, color: '#d95763', label: 'FIGHT 戰鬥' },
{ id: 'wake', icon: Sun, color: '#ffe762', label: 'WAKE 起床' },
{ id: 'pray', icon: Sparkles, color: '#e0d8f0', label: 'PRAY 祈福' },
{ id: 'shop', icon: ShoppingBag, color: '#ffa500', label: 'SHOP 商店' },
];
const gridItems = computed(() => {
return Array.from({ length: 12 }).map((_, i) => ACTIONS[i] || null);
});
const handCards = [
{ name: 'Slash', cost: 2, icon: Sword, color: '#d75b5b' },
{ name: 'Block', cost: 1, icon: Shield, color: '#f6b26b' },
{ name: 'Heal', cost: 3, icon: FlaskConical, color: '#9fd75b' }
];
const statsList = computed(() => {
const s = props.playerStats || { str:0, int:0, dex:0, luck:0, atk:0, def:0, spd:0 };
return [
{l:'STR', v:s.str}, {l:'ATK', v:s.atk, c: '#d75b5b'},
{l:'INT', v:s.int}, {l:'DEF', v:s.def, c: '#f6b26b'},
{l:'DEX', v:s.dex}, {l:'SPD', v:s.spd},
{l:'LCK', v:s.luck},
];
});
const handleActionClick = (id: string) => {
if (id === 'pray') emit('openGodSystem');
else if (id === 'shop') emit('openShop');
else if (id === 'fight') emit('openAdventure');
};
</script>

View File

@ -1,95 +0,0 @@
<template>
<div class="flex flex-col h-full bg-black text-[#99e550] relative">
<!-- Header -->
<div class="flex items-center gap-2 text-xl font-bold p-2 border-b-2 border-[#99e550]">
<Map class="text-[#e0d8f0]" />
<span class="tracking-widest">選擇冒險區域 (SELECT ZONE)</span>
<button @click="$emit('close')" class="ml-auto text-white hover:text-red-500"><X /></button>
</div>
<!-- Content -->
<div class="flex-grow overflow-y-auto p-4 custom-scrollbar flex flex-col gap-4">
<div
v-for="loc in locations"
:key="loc.id"
class="border-2 p-4 relative transition-all"
:class="isLocked(loc) ? 'border-gray-600 opacity-70' : 'border-[#99e550] hover:bg-[#0f2a0f]'"
>
<!-- Title -->
<div class="text-xl font-bold tracking-widest mb-2 text-[#99e550]">
{{ loc.name }}
</div>
<!-- Description -->
<p class="text-xs text-white mb-4 text-center">
{{ loc.description }}
</p>
<!-- Costs & Reqs -->
<div class="flex flex-wrap gap-4 mb-4 text-sm font-mono">
<div class="flex items-center gap-2">
<span class="text-[#99e550]">消耗:</span>
<div class="flex items-center gap-1" :class="canAffordHunger(loc) ? 'text-[#9fd75b]' : 'text-red-500'">
<Drumstick :size="14" /> {{ loc.costHunger }}
</div>
<div class="flex items-center gap-1" :class="canAffordGold(loc) ? 'text-[#f6b26b]' : 'text-red-500'">
<Coins :size="14" /> {{ loc.costGold }}
</div>
</div>
<div v-if="loc.reqStats" class="flex items-center gap-2">
<span class="text-[#99e550]">要求:</span>
<span v-if="loc.reqStats.str" :class="meetsStr(loc) ? 'text-[#9fd75b]' : 'text-red-500'">STR {{ loc.reqStats.str }}</span>
<span v-if="loc.reqStats.int" :class="meetsInt(loc) ? 'text-[#9fd75b]' : 'text-red-500'">INT {{ loc.reqStats.int }}</span>
</div>
</div>
<!-- Action Button -->
<button
@click="!isLocked(loc) && canAfford(loc) && $emit('selectLocation', loc)"
:disabled="!canAfford(loc) || isLocked(loc)"
class="w-full py-2 text-lg tracking-[0.2em] border"
:class="(!canAfford(loc) || isLocked(loc))
? 'border-gray-600 text-gray-500 cursor-not-allowed'
: 'border-[#d95763] text-[#d95763] hover:bg-[#d95763] hover:text-black'"
>
{{ isLocked(loc) ? "能力不足" : !canAfford(loc) ? "資源不足" : "出發 !" }}
</button>
<!-- Side Decoration Bar -->
<div class="absolute top-2 bottom-2 right-2 w-2" :class="isLocked(loc) ? 'bg-gray-600' : 'bg-[#99e550]'"></div>
</div>
</div>
<!-- Footer / Close Button -->
<div class="p-4 border-t border-[#99e550]">
<button
@click="$emit('close')"
class="border border-[#99e550] text-[#99e550] px-4 py-2 hover:bg-[#99e550] hover:text-black"
>
關閉
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { Map, Drumstick, Coins, X } from 'lucide-vue-next';
import type { AdventureLocation, EntityStats } from '~/types/pixel';
interface Props {
locations: AdventureLocation[];
playerStats: EntityStats;
}
const props = defineProps<Props>();
defineEmits(['selectLocation', 'close']);
const canAffordHunger = (loc: AdventureLocation) => (props.playerStats.hunger || 0) >= loc.costHunger;
const canAffordGold = (loc: AdventureLocation) => (props.playerStats.gold || 0) >= loc.costGold;
const meetsStr = (loc: AdventureLocation) => !loc.reqStats?.str || (props.playerStats.str || 0) >= loc.reqStats.str;
const meetsInt = (loc: AdventureLocation) => !loc.reqStats?.int || (props.playerStats.int || 0) >= loc.reqStats.int;
const isLocked = (loc: AdventureLocation) => !meetsStr(loc) || !meetsInt(loc);
const canAfford = (loc: AdventureLocation) => canAffordHunger(loc) && canAffordGold(loc);
</script>

View File

@ -1,99 +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 opacity-50"
:style="{
backgroundImage: `url('https://picsum.photos/seed/dungeon/800/400')`,
filter: 'contrast(1.2) brightness(0.5) sepia(0.5) hue-rotate(260deg) saturate(1.5)'
}"
/>
<!-- Pixelated Dither Overlay -->
<div
class="absolute inset-0 opacity-10 pointer-events-none"
:style="{
backgroundImage: `repeating-linear-gradient(45deg, #000 0, #000 1px, transparent 0, transparent 50%)`,
backgroundSize: '4px 4px'
}"
/>
<!-- Room Decor - Ground Rug/Circle -->
<div class="absolute bottom-12 left-1/2 transform -translate-x-1/2 w-48 h-12 bg-[#2b193f] opacity-60 rounded-[50%] border-2 border-[#4a3b5e] shadow-[0_0_20px_rgba(0,0,0,0.5)]"></div>
<!-- Battle Log Mode (If Fighting) -->
<div v-if="isFighting" class="absolute inset-0 bg-black/80 backdrop-blur-sm p-6">
<PixelFrame class="h-full flex flex-col">
<div class="text-[#2ce8f4] text-sm mb-4 uppercase tracking-widest font-bold">
BATTLE LOG
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar space-y-2 text-xs">
<div
v-for="(log, idx) in battleLogs"
:key="idx"
class="text-[#e0d8f0] animate-fade-in"
>
{{ log }}
</div>
</div>
</PixelFrame>
</div>
<!-- Main Pet Avatar (Center, Idle) -->
<div class="absolute bottom-16 left-1/2 transform -translate-x-1/2 z-10 scale-[3]">
<PixelAvatar
skinColor="#ffdbac"
hairColor="#e0d8f0"
outfitColor="#9fd75b"
:deityId="currentDeityId"
weapon="staff"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue';
import PixelAvatar from './PixelAvatar.vue';
import type { DeityId } from '~/types/pixel';
interface Props {
currentDeityId?: string;
isFighting?: boolean;
battleLogs?: string[];
}
const props = withDefaults(defineProps<Props>(), {
isFighting: false,
battleLogs: () => []
});
const logEndRef = ref<HTMLDivElement | null>(null);
watch(() => props.battleLogs, async () => {
await nextTick();
if (logEndRef.value) {
logEndRef.value.scrollIntoView({ behavior: 'smooth' });
}
}, { deep: true });
</script>

View File

@ -1,322 +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">
[GOD SYSTEM] 神明系統
</div>
<!-- Top Action Buttons Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
<PixelButton
v-for="action in TAB_ACTIONS"
:key="action.id"
@click="activeTab = action.id"
:variant="activeTab === action.id ? 'primary' : 'secondary'"
class="text-xs md:text-sm flex items-center justify-center gap-2"
>
<component :is="action.icon" :size="16" />
{{ action.label }}
</PixelButton>
<PixelButton :variant="activeTab === 'LIST' ? 'primary' : 'secondary'" @click="activeTab = 'LIST'" class="text-xs md:text-sm">
[LIST] 神明列表
</PixelButton>
</div>
<!-- Main Content Area -->
<div class="flex-grow bg-[#150c1f] border border-[#4a3b5e] p-4 relative overflow-hidden">
<!-- Background Decor -->
<div class="absolute inset-0 opacity-20 pointer-events-none flex items-center justify-center">
<div class="w-64 h-64 border-[20px] border-[#2b193f] rounded-full"></div>
</div>
<!-- --- VIEW: PRAY --- -->
<div v-if="activeTab === 'PRAY'" class="flex flex-col items-center justify-center h-full gap-6">
<div class="transform scale-150 mb-4">
<PixelAvatar :deityId="currentDeity" />
</div>
<h2 class="text-2xl text-[#f6b26b] font-bold">{{ activeDeity.title }} {{ activeDeity.name }}</h2>
<div class="w-full max-w-md">
<div class="flex justify-between text-xs text-[#8f80a0] mb-1">
<span>FAVOR (好感度)</span>
<span>{{ activeDeity.favor }}/{{ activeDeity.maxFavor }}</span>
</div>
<div class="h-4 bg-[#2b193f] border border-[#4a3b5e] rounded-full overflow-hidden relative">
<div
class="h-full bg-[#d95763] transition-all duration-500"
:style="{ width: `${(activeDeity.favor / activeDeity.maxFavor) * 100}%` }"
></div>
</div>
</div>
<PixelButton class="w-48 py-4 text-lg animate-pulse" @click="$emit('addFavor', 10)">
🙏 PRAY (祈福)
</PixelButton>
<p class="text-xs text-[#8f80a0]">Increases favor with {{ activeDeity.name }}</p>
</div>
<!-- --- VIEW: JIAOBEI (Free Toss) --- -->
<div v-else-if="activeTab === 'JIAOBEI'" class="flex flex-col items-center justify-center h-full gap-8">
<h3 class="text-[#f6b26b] text-lg uppercase tracking-widest">Moon Block Divination</h3>
<JiaobeiBlocks :result="lastResult" :isTossing="isTossing" />
<div class="mt-8">
<PixelButton @click="handleToss(false)" :disabled="isTossing" class="w-40">
{{ isTossing ? 'TOSSING...' : 'TOSS BLOCKS' }}
</PixelButton>
</div>
</div>
<!-- --- VIEW: LOT (Draw & Verify) --- -->
<div v-else-if="activeTab === 'LOT'" class="flex flex-col items-center justify-center h-full gap-4 text-center w-full">
<!-- Phase: Idle -->
<template v-if="lotPhase === LotPhase.Idle">
<Scroll :size="64" class="text-[#ffe762] mb-4" />
<h3 class="text-xl text-[#e0d8f0] mb-2">Draw a Fortune Lot</h3>
<p class="text-sm text-[#8f80a0] max-w-xs mb-6">
Shake the container to draw a stick, then verify it with 3 consecutive Saint Cups.
</p>
<PixelButton @click="handleDrawLot" class="w-48">
DRAW LOT
</PixelButton>
</template>
<!-- Phase: Drawing (Animation) -->
<div v-else-if="lotPhase === LotPhase.Drawing" class="animate-bounce">
<div class="w-16 h-24 bg-[#4a2e18] border-2 border-[#f6b26b] mx-auto mb-4 relative rounded-sm">
<div class="absolute top-0 left-0 w-full h-full flex items-center justify-center text-[#f6b26b] font-bold">
...
</div>
</div>
<span class="text-[#f6b26b] tracking-widest">SHAKING...</span>
</div>
<!-- Phase: Verify -->
<template v-else-if="lotPhase === LotPhase.PendingVerify || lotPhase === LotPhase.Verifying">
<div class="text-2xl font-bold text-[#e0d8f0] border-2 border-[#f6b26b] px-4 py-2 mb-4 bg-[#2b193f]">
LOT #{{ drawnLotNumber }}
</div>
<p class="text-sm text-[#8f80a0] mb-4">
Verify with 3 Consecutive Saint Cups
</p>
<div class="flex gap-2 mb-6 justify-center">
<div
v-for="i in 3"
:key="i"
class="w-4 h-4 rounded-full border border-[#4a3b5e]"
:class="i <= saintCupCount ? 'bg-[#d95763] shadow-[0_0_10px_#d95763]' : 'bg-[#1b1026]'"
/>
</div>
<JiaobeiBlocks :result="lastResult" :isTossing="isTossing" />
<div class="mt-8">
<PixelButton @click="handleToss(true)" :disabled="isTossing" class="w-40">
VERIFY ({{ saintCupCount }}/3)
</PixelButton>
</div>
</template>
<!-- Phase: Failed -->
<template v-else-if="lotPhase === LotPhase.Failed">
<div class="text-[#d95763] text-4xl mb-4"></div>
<h3 class="text-lg text-[#d95763] mb-2">Not a Saint Cup</h3>
<p class="text-sm text-[#8f80a0] mb-6">
The deity indicates this is not the right lot.<br/>Please draw again.
</p>
<PixelButton @click="resetLot" variant="danger">
TRY AGAIN
</PixelButton>
</template>
<!-- Phase: Success - Detailed Result -->
<div v-else-if="lotPhase === LotPhase.Success" class="w-full h-full overflow-y-auto custom-scrollbar p-2">
<PixelFrame class="bg-[#1b1026] border-4 border-[#f6b26b] relative shadow-[0_0_20px_rgba(246,178,107,0.3)]">
<!-- Header -->
<div class="text-center border-b-2 border-[#4a3b5e] pb-3 mb-3 bg-[#231533] p-2">
<div class="text-[#99e550] text-sm md:text-lg font-bold mb-1 flex items-center justify-center gap-2 animate-pulse">
<Sparkles :size="16" />
<span>三聖筊{{ activeDeity.name }}允准解籤</span>
<Sparkles :size="16" />
</div>
<div class="text-[#f6b26b] text-xl md:text-3xl font-bold tracking-widest mt-2 font-serif">
{{ LOT_RESULT_DATA.number }} {{ LOT_RESULT_DATA.level }}
</div>
</div>
<!-- Poem (Block) -->
<div class="bg-[#2b193f] p-4 text-center mb-4 border-l-4 border-[#f6b26b] mx-2 shadow-inner">
<div v-for="(line, i) in LOT_RESULT_DATA.poem" :key="i" class="text-lg md:text-xl text-[#e0d8f0] tracking-[0.2em] leading-loose font-serif drop-shadow-md">
{{ line }}
</div>
</div>
<!-- Details Grid -->
<div class="flex flex-col gap-4 text-left px-2">
<!-- Meaning -->
<div class="bg-[#0f0816] p-2 border border-[#4a3b5e]">
<span class="text-[#f6b26b] font-bold text-sm block mb-1 border-b border-[#4a3b5e] pb-1 w-full">
解曰 Meaning
</span>
<p class="text-[#e0d8f0] text-sm leading-relaxed mt-1">{{ LOT_RESULT_DATA.meaning }}</p>
</div>
<!-- Interpretation -->
<div class="bg-[#0f0816] p-2 border border-[#4a3b5e]">
<span class="text-[#99e550] font-bold text-sm block mb-1 border-b border-[#4a3b5e] pb-1 w-full">
解籤 Interpretation
</span>
<p class="text-[#e0d8f0] text-sm leading-relaxed mt-1">{{ LOT_RESULT_DATA.interpretation }}</p>
</div>
<!-- Story -->
<div class="bg-[#0f0816] p-2 border border-[#4a3b5e]">
<span class="text-[#2ce8f4] font-bold text-sm block mb-1 border-b border-[#4a3b5e] pb-1 w-full">
典故 Story: {{ LOT_RESULT_DATA.storyTitle }}
</span>
<div class="text-[#8f80a0] text-xs leading-relaxed mt-1">
{{ LOT_RESULT_DATA.story }}
</div>
</div>
</div>
<div class="mt-6 flex justify-center mb-2">
<PixelButton @click="resetLot" class="w-full md:w-auto px-8 py-3">
收入背包 (KEEP LOT)
</PixelButton>
</div>
</PixelFrame>
</div>
</div>
<!-- --- VIEW: LIST/SWITCH --- -->
<div v-else-if="activeTab === 'LIST' || activeTab === 'VERIFY'" class="flex flex-col gap-4">
<div class="text-xs text-[#8f80a0] uppercase mb-2"> [SWITCH] 切換神明</div>
<div class="grid grid-cols-2 gap-4">
<button
v-for="deity in Object.values(deities)"
:key="deity.id"
@click="$emit('switchDeity', deity.id)"
class="border-2 p-3 flex items-center gap-3 transition-all relative"
:class="currentDeity === deity.id ? 'border-[#99e550] bg-[#2b193f]' : 'border-[#4a3b5e] bg-[#0f0816] hover:bg-[#150c1f]'"
>
<div class="w-10 h-10 relative">
<PixelAvatar :deityId="deity.id" />
</div>
<div class="flex flex-col items-start">
<span class="font-bold" :class="currentDeity === deity.id ? 'text-[#99e550]' : 'text-[#e0d8f0]'">
{{ deity.name }}
</span>
<span class="text-[10px] text-[#8f80a0]">{{ deity.title }}</span>
</div>
<div v-if="currentDeity === deity.id" class="absolute top-2 right-2 w-2 h-2 bg-[#99e550] rounded-full shadow-[0_0_5px_#99e550]"></div>
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { Heart, Sparkles, Scroll, Repeat, CheckCircle2 } from 'lucide-vue-next';
import PixelButton from './PixelButton.vue';
import PixelAvatar from './PixelAvatar.vue';
import PixelFrame from './PixelFrame.vue';
import JiaobeiBlocks from './JiaobeiBlocks.vue';
import { DeityId, JiaobeiResult, LotPhase } from '~/types/pixel';
import type { Deity } from '~/types/pixel';
interface Props {
currentDeity: DeityId;
deities: Record<DeityId, Deity>;
}
const props = defineProps<Props>();
defineEmits(['switchDeity', 'addFavor']);
const activeTab = ref('PRAY');
const isTossing = ref(false);
const lastResult = ref<JiaobeiResult | null>(null);
const lotPhase = ref<LotPhase>(LotPhase.Idle);
const drawnLotNumber = ref<number | null>(null);
const saintCupCount = ref(0);
const TAB_ACTIONS = [
{ id: 'PRAY', label: 'PRAY (祈福)', icon: Sparkles },
{ id: 'LOT', label: 'LOT (求籤)', icon: Scroll },
{ id: 'VERIFY', label: 'VERIFY (驗證)', icon: CheckCircle2 },
{ id: 'JIAOBEI', label: 'JIAOBEI (擲筊)', icon: Repeat },
];
const LOT_RESULT_DATA = {
number: "第八十六籤",
level: "上籤",
poem: [
"春來花發映陽臺",
"萬里舟行進寶來",
"躍過禹門三級浪",
"恰如平地一聲雷"
],
meaning: "此卦上朝見帝之象。凡事太吉大利也。",
interpretation: "朝帝受職。如貧得寶。謀望從心。卦中第一。此籤從心所欲。諸事皆吉。",
storyTitle: "商絡中三元",
story: "三元記。明朝。商絡。浙江人。父早亡。商絡三元及第。喻步步高升也。(三元即三級試。鄉試解元。省試會元。殿試狀元)"
};
const activeDeity = computed(() => props.deities[props.currentDeity]);
const calculateToss = (): JiaobeiResult => {
const rand = Math.random();
if (rand < 0.5) return JiaobeiResult.Saint;
if (rand < 0.75) return JiaobeiResult.Smile;
return JiaobeiResult.Cry;
};
const handleToss = (isLotVerify = false) => {
if (isTossing.value) return;
isTossing.value = true;
lastResult.value = null;
setTimeout(() => {
const result = calculateToss();
isTossing.value = false;
lastResult.value = result;
if (isLotVerify) {
if (result === JiaobeiResult.Saint) {
saintCupCount.value++;
if (saintCupCount.value >= 3) {
lotPhase.value = LotPhase.Success;
}
} else {
lotPhase.value = LotPhase.Failed;
}
}
}, 1500);
};
const handleDrawLot = () => {
lotPhase.value = LotPhase.Drawing;
setTimeout(() => {
drawnLotNumber.value = Math.floor(Math.random() * 60) + 1;
lotPhase.value = LotPhase.PendingVerify;
saintCupCount.value = 0;
lastResult.value = null;
}, 2000);
};
const resetLot = () => {
lotPhase.value = LotPhase.Idle;
drawnLotNumber.value = null;
saintCupCount.value = 0;
lastResult.value = null;
};
</script>

View File

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

View File

@ -1,209 +0,0 @@
<template>
<div class="flex flex-col h-full gap-2">
<!-- 1. Rarity Legend -->
<div class="flex flex-wrap gap-2 px-2 py-1 bg-[#150c1f] border border-[#4a3b5e] text-[10px]">
<span class="text-[#8f80a0] mr-2">Rarity:</span>
<div v-for="(color, rarity) in RARITY_COLORS" :key="rarity" class="flex items-center gap-1 border border-[#2b193f] px-1 bg-[#0f0816]">
<span :style="{ color: color }">{{ rarity }}</span>
<span class="text-[#4a3b5e]">(10%)</span>
</div>
</div>
<!-- 2. Equipment Slots Grid -->
<div class="grid grid-cols-2 md:grid-cols-3 gap-2">
<div v-for="slot in Object.values(EquipSlot)" :key="slot" class="border border-[#4a3b5e] bg-[#0f0816] p-2 flex flex-col gap-2 relative">
<!-- Slot Header -->
<div class="flex items-center gap-2 mb-1 justify-center border-b border-[#2b193f] pb-1">
<component :is="SLOT_ICONS[slot]" :size="14" class="text-[#8f80a0]" />
<span class="text-[#2ce8f4] text-xs font-bold uppercase tracking-wider">{{ slot }}</span>
</div>
<!-- Actual Slot -->
<div
class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center cursor-pointer hover:border-[#9fd75b] group"
@click="getEquippedItem(slot, false) && setSelectedItemId(getEquippedItem(slot, false)?.id)"
>
<span class="text-[9px] text-[#8f80a0] mb-0.5">ACTUAL</span>
<span v-if="getEquippedItem(slot, false)" class="text-xs text-center" :style="{ color: RARITY_COLORS[getEquippedItem(slot, false)!.rarity] }">{{ getEquippedItem(slot, false)!.name }}</span>
<span v-else class="text-[10px] text-[#4a3b5e]">Empty</span>
</div>
<!-- Appearance Slot -->
<div
class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center cursor-pointer hover:border-[#d584fb] group"
@click="getEquippedItem(slot, true) && setSelectedItemId(getEquippedItem(slot, true)?.id)"
>
<span class="text-[9px] text-[#8f80a0] mb-0.5">COSMETIC</span>
<span v-if="getEquippedItem(slot, true)" class="text-xs text-center" :style="{ color: RARITY_COLORS[getEquippedItem(slot, true)!.rarity] }">{{ getEquippedItem(slot, true)!.name }}</span>
<span v-else class="text-[10px] text-[#4a3b5e]">Empty</span>
</div>
</div>
</div>
<!-- 3. Backpack Section -->
<div class="flex-grow flex flex-col md:flex-row gap-2 overflow-hidden mt-2">
<!-- Item Grid -->
<PixelFrame class="flex-grow flex flex-col bg-[#1b1026]" :title="`Backpack (${items.filter(i => !i.isEquipped).length})`">
<div class="flex-grow overflow-y-auto p-1 custom-scrollbar">
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2">
<button
v-for="item in items.filter(i => !i.isEquipped)"
:key="item.id"
@click="setSelectedItemId(item.id)"
class="relative p-2 flex flex-col items-center justify-center gap-1 min-h-[80px] border-2 transition-all group bg-[#2b193f]"
:class="selectedItemId === item.id ? 'border-white bg-[#3d2459]' : 'border-[#4a3b5e] hover:border-[#8f80a0]'"
>
<div class="relative">
<!-- Generic Icons based on type -->
<template v-if="item.type === ItemType.Equipment">
<Sword v-if="item.slot === EquipSlot.Weapon" :color="RARITY_COLORS[item.rarity]" />
<Shield v-else-if="item.slot === EquipSlot.Armor" :color="RARITY_COLORS[item.rarity]" />
<Crown v-else-if="item.slot === EquipSlot.Hat" :color="RARITY_COLORS[item.rarity]" />
<Gem v-else-if="item.slot === EquipSlot.Accessory" :color="RARITY_COLORS[item.rarity]" />
<Sparkles v-else-if="item.slot === EquipSlot.Charm" :color="RARITY_COLORS[item.rarity]" />
<Star v-else :color="RARITY_COLORS[item.rarity]" />
</template>
<template v-else>
<Zap v-if="item.name.includes('Potion')" :color="RARITY_COLORS[item.rarity]" />
<Heart v-else :color="RARITY_COLORS[item.rarity]" />
</template>
<span v-if="item.quantity && item.quantity > 1" class="absolute -bottom-2 -right-2 text-[10px] bg-black text-white px-1 border border-[#4a3b5e]">{{ item.quantity }}</span>
</div>
<span class="text-[10px] text-center leading-tight line-clamp-2" :style="{ color: RARITY_COLORS[item.rarity] }">
{{ item.name }}
</span>
</button>
</div>
</div>
</PixelFrame>
<!-- Selected Item Detail -->
<div class="w-full md:w-1/3 min-h-[200px] flex-shrink-0">
<PixelFrame v-if="selectedItem" class="h-full bg-[#150c1f] flex flex-col" highlight>
<!-- Item Header -->
<div class="flex gap-3 mb-2 border-b border-[#4a3b5e] pb-2">
<div class="w-12 h-12 bg-[#0f0816] border border-[#4a3b5e] flex items-center justify-center">
<Shirt v-if="selectedItem.type === ItemType.Equipment" :size="24" :color="RARITY_COLORS[selectedItem.rarity]" />
<Zap v-else :size="24" :color="RARITY_COLORS[selectedItem.rarity]" />
</div>
<div class="flex flex-col">
<span class="font-bold text-sm tracking-wide" :style="{ color: RARITY_COLORS[selectedItem.rarity] }">{{ selectedItem.name }}</span>
<div class="flex gap-2 text-[10px] text-[#8f80a0]">
<span>{{ selectedItem.rarity }}</span>
<span></span>
<span>{{ selectedItem.type }}</span>
</div>
</div>
</div>
<!-- Description -->
<div class="mb-2">
<p class="text-xs text-[#e0d8f0] italic mb-2">"{{ selectedItem.description }}"</p>
<!-- Stats Block -->
<div v-if="selectedItem.statsDescription" class="bg-[#0f0816] border border-[#4a3b5e] p-2 mb-2">
<span class="text-[10px] text-[#99e550] block mb-1">EFFECTS:</span>
<span class="text-xs text-[#2ce8f4]">{{ selectedItem.statsDescription }}</span>
</div>
<div v-if="selectedItem.effects && selectedItem.effects.length > 0" class="flex flex-col gap-1">
<span v-for="(eff, i) in selectedItem.effects" :key="i" class="text-[10px] text-[#9fd75b]">+ {{ eff }}</span>
</div>
</div>
<!-- Actions -->
<div class="mt-auto flex flex-col gap-2">
<div v-if="selectedItem.type === ItemType.Equipment" class="grid grid-cols-2 gap-2">
<PixelButton
class="text-[10px] py-1"
:disabled="selectedItem.isEquipped && !selectedItem.isAppearance"
@click="$emit('equip', selectedItem.id, false)"
>
{{ selectedItem.isEquipped && !selectedItem.isAppearance ? 'EQUIPPED' : 'EQUIP' }}
</PixelButton>
<PixelButton
variant="secondary"
class="text-[10px] py-1"
:disabled="selectedItem.isEquipped && selectedItem.isAppearance"
@click="$emit('equip', selectedItem.id, true)"
>
COSMETIC
</PixelButton>
</div>
<PixelButton v-if="selectedItem.type === ItemType.Consumable" @click="$emit('use', selectedItem.id)">USE ITEM</PixelButton>
<div class="flex justify-between mt-2 pt-2 border-t border-[#4a3b5e]">
<button
v-if="selectedItem.isEquipped"
@click="$emit('unequip', selectedItem.slot!, selectedItem.isAppearance!)"
class="text-[#f6b26b] text-xs hover:underline"
>
Unequip
</button>
<button
@click="$emit('delete', selectedItem.id)"
class="text-[#d95763] text-xs hover:text-red-400 flex items-center gap-1 ml-auto"
>
<Trash2 :size="10" /> Delete
</button>
</div>
</div>
</PixelFrame>
<PixelFrame v-else class="h-full bg-[#150c1f] flex items-center justify-center text-[#4a3b5e]">
<div class="text-center">
<HelpCircle :size="32" class="mx-auto mb-2 opacity-50" />
<span class="text-xs">Select an item<br/>to view details</span>
</div>
</PixelFrame>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { Sword, Shield, Crown, Gem, Sparkles, Star, Shirt, HelpCircle, Trash2, Zap, Heart } from 'lucide-vue-next';
import PixelFrame from './PixelFrame.vue';
import PixelButton from './PixelButton.vue';
import { ItemType, EquipSlot, Rarity } from '~/types/pixel';
import type { Item } from '~/types/pixel';
interface Props {
items: Item[];
}
const props = defineProps<Props>();
defineEmits(['equip', 'unequip', 'use', 'delete']);
const selectedItemId = ref<string | null>(null);
const selectedItem = computed(() => props.items.find(i => i.id === selectedItemId.value));
const RARITY_COLORS: Record<Rarity, string> = {
[Rarity.Common]: '#9ca3af', // Gray
[Rarity.Excellent]: '#9fd75b', // Green
[Rarity.Rare]: '#2ce8f4', // Blue
[Rarity.Epic]: '#d584fb', // Purple
[Rarity.Legendary]: '#ffa500', // Orange
};
const SLOT_ICONS: Record<EquipSlot, any> = {
[EquipSlot.Weapon]: Sword,
[EquipSlot.Armor]: Shield,
[EquipSlot.Hat]: Crown,
[EquipSlot.Accessory]: Gem,
[EquipSlot.Charm]: Sparkles,
[EquipSlot.Special]: Star,
};
const getEquippedItem = (slot: EquipSlot, isAppearance: boolean) => {
return props.items.find(i => i.isEquipped && i.slot === slot && !!i.isAppearance === isAppearance);
};
</script>

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 type { JiaobeiResult } from '~/types/pixel';
interface Props {
result: JiaobeiResult | null;
isTossing: boolean;
}
defineProps<Props>();
</script>

View File

@ -1,80 +0,0 @@
<template>
<div class="w-12 h-12 relative image-pixelated">
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" shapeRendering="crispEdges">
<!-- Body/Head -->
<rect x="6" y="2" width="4" height="4" :fill="finalSkin" /> <!-- Head -->
<rect x="5" y="6" width="6" height="5" :fill="finalOutfit" /> <!-- Body -->
<rect x="5" y="6" width="2" height="3" :fill="finalOutfit" filter="brightness(0.9)" /> <!-- Left Arm -->
<rect x="9" y="6" width="2" height="3" :fill="finalOutfit" filter="brightness(0.9)" /> <!-- Right Arm -->
<rect x="6" y="11" width="1" height="3" :fill="finalSkin" /> <!-- Leg L -->
<rect x="9" y="11" width="1" height="3" :fill="finalSkin" /> <!-- Leg R -->
<!-- Hair -->
<rect x="5" y="1" width="6" height="2" :fill="finalHair" />
<rect x="4" y="2" width="1" height="3" :fill="finalHair" />
<rect x="11" y="2" width="1" height="3" :fill="finalHair" />
<!-- Face -->
<rect x="7" y="4" width="1" height="1" fill="#000" opacity="0.6"/> <!-- Eye -->
<rect x="9" y="4" width="1" height="1" fill="#000" opacity="0.6"/> <!-- Eye -->
<!-- Accessory/Deity Specifics -->
<rect v-if="deityId === 'Mazu'" x="5" y="0" width="6" height="1" fill="#d4af37" /> <!-- Crown -->
<rect v-if="deityId === 'EarthGod'" x="5" y="10" width="6" height="1" fill="#8e5c2e" /> <!-- Belt -->
<rect v-if="deityId === 'Matchmaker'" x="10" y="7" width="2" height="2" fill="#ff0000" /> <!-- Red Thread -->
<!-- Weapon -->
<path v-if="weapon === 'sword'" d="M11 9 L13 7 L14 8 L12 10 Z" fill="#ccc" />
<rect v-if="weapon === 'staff'" x="11" y="5" width="1" height="8" fill="#8d6e63" />
</svg>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
skinColor?: string;
hairColor?: string;
outfitColor?: string;
weapon?: 'none' | 'sword' | 'staff';
deityId?: string;
}
const props = withDefaults(defineProps<Props>(), {
skinColor: '#ffdbac',
hairColor: '#5e412f',
outfitColor: '#78909c',
weapon: 'none'
});
const finalSkin = computed(() => {
if (props.deityId === 'Mazu') return '#ffe0bd';
if (props.deityId === 'EarthGod') return '#f0c0a8';
if (props.deityId === 'Matchmaker') return '#ffe0bd';
if (props.deityId === 'Wenchang') return '#ffe0bd';
return props.skinColor;
});
const finalHair = computed(() => {
if (props.deityId === 'Mazu') return '#1a1a1a';
if (props.deityId === 'EarthGod') return '#f0f0f0';
if (props.deityId === 'Matchmaker') return '#f0f0f0';
if (props.deityId === 'Wenchang') return '#1a1a1a';
return props.hairColor;
});
const finalOutfit = computed(() => {
if (props.deityId === 'Mazu') return '#ffa500';
if (props.deityId === 'EarthGod') return '#d75b5b';
if (props.deityId === 'Matchmaker') return '#d95763';
if (props.deityId === 'Wenchang') return '#9fd75b';
return props.outfitColor;
});
</script>
<style scoped>
.image-pixelated {
image-rendering: pixelated;
}
</style>

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,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,111 +0,0 @@
<template>
<div class="h-full flex flex-col p-4 gap-4 bg-[#1b1026] overflow-y-auto custom-scrollbar">
<!-- Pet Avatar -->Portrait & Basic Info -->
<PixelFrame class="flex-shrink-0" title="PET INFO">
<!-- Helper Buttons Overlay -->
<div class="absolute top-1 right-1 z-30">
<button
@click="$emit('openAchievements')"
class="p-1 bg-[#2b193f] border border-[#f6b26b] hover:bg-[#3d2459] active:translate-y-0.5 group"
title="Achievements"
>
<Trophy :size="14" class="text-[#f6b26b] group-hover:text-white" />
</button>
</div>
<div class="flex flex-col items-center p-1 relative">
<div class="w-20 h-20 bg-[#1b1026] border-4 border-[#4a3b5e] mb-2 relative overflow-hidden group shadow-inner flex items-center justify-center">
<!-- Background for portrait -->
<div class="absolute inset-0 bg-[#2b193f] opacity-50" />
<!-- The Animated Pixel Avatar -->
<div class="scale-110 transform translate-y-1">
<PixelAvatar
skinColor="#ffdbac"
hairColor="#e0d8f0"
outfitColor="#9fd75b"
/>
</div>
<!-- Scanline on portrait -->
<div class="absolute inset-0 bg-[linear-gradient(rgba(0,0,0,0)_50%,rgba(0,0,0,0.2)_50%)] bg-[length:100%_4px] pointer-events-none z-20" />
</div>
<h2 class="text-xl text-[#f6b26b] tracking-[0.2em] font-bold border-b-2 border-[#f6b26b] mb-1 leading-none pb-1">{{ stats.name }}</h2>
<span class="text-xs text-[#8f80a0] uppercase tracking-wide">{{ stats.class }}</span>
</div>
</PixelFrame>
<!-- Vitals - Updated to Health, Hunger, Happiness -->
<div class="flex flex-col gap-1 px-1">
<RetroResourceBar :current="stats.hp" :max="stats.maxHp" type="hp" label="Health" :icon="Heart" />
<RetroResourceBar v-if="stats.hunger !== undefined" :current="stats.hunger" :max="stats.maxHunger || 100" type="energy" label="Hunger" :icon="Drumstick" />
<RetroResourceBar v-if="stats.happiness !== undefined" :current="stats.happiness" :max="stats.maxHappiness || 100" type="mana" label="Happy" :icon="Smile" />
</div>
<!-- Pet Details Grid -->
<PixelFrame class="flex-shrink-0 mt-1" variant="inset">
<div class="grid grid-cols-2 gap-x-2 gap-y-2 text-[10px] uppercase text-[#8f80a0]">
<div class="flex flex-col border-r border-[#4a3b5e] pr-1">
<span class="text-[#4a3b5e]">Age</span>
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.age }}</span>
</div>
<div class="flex flex-col pl-1">
<span class="text-[#4a3b5e]">Gen</span>
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.generation }}</span>
</div>
<div class="flex items-center gap-1 border-t border-[#4a3b5e] pt-1 col-span-2">
<Ruler :size="10" />
<span class="text-[#e0d8f0]">{{ stats.height }}</span>
<span class="text-[#4a3b5e] mx-1">|</span>
<Scale :size="10" />
<span class="text-[#e0d8f0]">{{ stats.weight }}</span>
</div>
</div>
</PixelFrame>
<!-- Fate & God Favor -->
<div class="flex flex-col gap-2 mt-2 px-1">
<!-- Fate -->
<div v-if="stats.fate" class="flex items-center gap-2 bg-[#2b193f] p-1 border border-[#4a3b5e] rounded">
<Leaf :size="12" color="#99e550" />
<div class="flex flex-col leading-none">
<span class="text-[8px] text-[#8f80a0] uppercase">Fate</span>
<span class="text-[10px] text-[#e0d8f0] tracking-wide">{{ stats.fate }}</span>
</div>
</div>
<!-- God Favor -->
<div v-if="stats.godFavor" class="flex flex-col gap-1">
<div class="flex justify-between text-[10px] text-[#8f80a0] uppercase">
<span>Favor: {{ stats.godFavor.name }}</span>
<span>{{ stats.godFavor.current }}/{{ stats.godFavor.max }}</span>
</div>
<div class="h-2 bg-[#150c1f] border border-[#4a3b5e] rounded-full overflow-hidden">
<div :style="{ width: `${(stats.godFavor.current / stats.godFavor.max) * 100}%` }" class="h-full bg-[#f6b26b]" />
</div>
</div>
</div>
<!-- Gold -->
<div class="mt-auto px-1 pb-1">
<RetroCounter :icon="Coins" :value="stats.gold || 0" color="#ffe762" />
</div>
</div>
</template>
<script setup lang="ts">
import { Ruler, Scale, Heart, Smile, Drumstick, Coins, Leaf, Trophy } from 'lucide-vue-next';
import PixelFrame from './PixelFrame.vue';
import RetroResourceBar from './RetroResourceBar.vue';
import RetroCounter from './RetroCounter.vue';
import PixelAvatar from './PixelAvatar.vue';
import type { EntityStats } from '~/types/pixel';
interface Props {
stats: EntityStats;
}
defineProps<Props>();
defineEmits(['openAchievements']);
</script>

View File

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

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,43 +0,0 @@
<template>
<div class="flex flex-col gap-0.5 w-full">
<div v-if="label" class="flex justify-between items-end px-0.5">
<div class="flex items-center gap-1 text-[#8f80a0]">
<component :is="icon" v-if="icon" :size="10" />
<span class="text-[10px] uppercase font-bold leading-none">{{ label }}</span>
</div>
<span class="text-[10px] font-mono text-[#e0d8f0] leading-none">{{ current }}/{{ max }}</span>
</div>
<div class="h-3 bg-[#0f0816] border border-[#4a3b5e] p-[1px] relative">
<div
class="h-full transition-all duration-300 relative"
:style="{ width: `${percentage}%`, backgroundColor: barColor }"
>
<!-- Shine effect -->
<div class="absolute top-0 left-0 w-full h-[1px] bg-white opacity-30" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import type { Component } from 'vue';
interface Props {
current: number;
max: number;
type: 'hp' | 'energy' | 'mana';
label?: string;
icon?: Component;
}
const props = defineProps<Props>();
const percentage = computed(() => Math.min(100, Math.max(0, (props.current / props.max) * 100)));
const barColor = computed(() => {
if (props.type === 'energy') return '#f6b26b'; // Orange
if (props.type === 'mana') return '#2ce8f4'; // Cyan
return '#d95763'; // HP Red (default)
});
</script>

View File

@ -1,145 +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 } from 'lucide-vue-next';
import PixelButton from './PixelButton.vue';
import { ItemCategory, Rarity } from '~/types/pixel';
import type { Item } from '~/types/pixel';
interface Props {
playerGold: number;
inventory: Item[];
shopItems: Item[];
}
const props = defineProps<Props>();
defineEmits(['buy', 'sell']);
const mode = ref<'BUY' | 'SELL'>('BUY');
const filter = ref<string>('ALL');
const CATEGORY_FILTERS = [
{ id: 'ALL', label: '全部 (ALL)', icon: Search },
{ id: ItemCategory.Food, label: '食物', icon: Cookie },
{ id: ItemCategory.Medicine, label: '藥品', icon: Pill },
{ id: ItemCategory.Equipment, label: '裝備', icon: Sword },
{ id: ItemCategory.Toy, label: '玩具', icon: Gamepad2 },
{ id: ItemCategory.Accessory, label: '飾品', icon: Gem },
];
const displayedItems = computed(() => {
const source = mode.value === 'BUY' ? props.shopItems : props.inventory;
return source.filter(item => {
if (mode.value === 'SELL' && item.isEquipped) return false; // Cannot sell equipped items
if (filter.value === 'ALL') return true;
return item.category === filter.value;
});
});
</script>

View File

@ -1,435 +0,0 @@
<template>
<div class="w-full min-h-screen bg-[#1b1026] flex items-center justify-center p-2 md:p-4 lg:p-8 font-sans">
<!-- Main Container -->
<div class="w-full max-w-7xl bg-[#0f0816] border-4 md:border-6 border-[#2b193f] relative shadow-2xl flex flex-col md:flex-row overflow-hidden rounded-lg"
:class="{'aspect-video': isDesktop, 'min-h-screen': !isDesktop}">
<!-- Left Column: Player Panel -->
<div class="w-full md:w-1/3 lg:w-1/4 h-auto md:h-full border-b-4 md:border-b-0 md:border-r-4 border-[#2b193f] bg-[#1b1026] z-20">
<PlayerPanel
v-if="initialized"
:stats="playerStats"
@openAchievements="showAchievements = true"
/>
<div v-else class="flex items-center justify-center h-32 md:h-full text-[#8f80a0] text-xs">
Initializing...
</div>
</div>
<!-- Middle Column: Room + Actions -->
<div class="w-full md:w-1/3 lg:w-1/2 h-auto md:h-full flex flex-col relative z-10">
<!-- Top: Battle/Room Area -->
<div class="h-64 md:h-[55%] border-b-4 border-[#2b193f] relative bg-[#0f0816]">
<BattleArea
v-if="initialized"
:currentDeityId="currentDeity"
:isFighting="isFighting"
:battleLogs="battleLogs"
/>
</div>
<!-- Bottom: Action Area -->
<div class="h-auto md:h-[45%] bg-[#1b1026]">
<ActionArea
v-if="initialized"
:playerStats="playerStats"
@openInventory="showInventory = true"
@openGodSystem="showGodSystem = true"
@openShop="showShop = true"
@openAdventure="showAdventureSelect = true"
/>
</div>
</div>
<!-- Right Column: Deity Panel (Info Panel) -->
<div class="w-full md:w-1/3 lg:w-1/4 h-auto md:h-full border-t-4 md:border-t-0 md:border-l-4 border-[#2b193f] bg-[#1b1026] z-20">
<InfoPanel v-if="deities[currentDeity]" :deity="deities[currentDeity]" />
<div v-else class="flex items-center justify-center h-32 md:h-full text-[#8f80a0] text-xs">
Loading...
</div>
</div>
</div>
<!-- --- MODALS --- -->
<!-- Achievements Overlay -->
<PixelModal
:isOpen="showAchievements"
@close="showAchievements = false"
title="ACHIEVEMENTS"
>
<AchievementsOverlay :achievements="ACHIEVEMENTS_DATA" />
</PixelModal>
<!-- Inventory Overlay -->
<PixelModal
:isOpen="showInventory"
@close="showInventory = false"
title="INVENTORY"
>
<InventoryOverlay
:items="inventory"
@equip="handleEquip"
@unequip="handleUnequip"
@use="handleUseItem"
@delete="handleDeleteItem"
/>
</PixelModal>
<!-- God System Overlay -->
<PixelModal
:isOpen="showGodSystem"
@close="showGodSystem = false"
title="GOD SYSTEM"
>
<GodSystemOverlay
:currentDeity="currentDeity"
:deities="deities"
@switchDeity="handleSwitchDeity"
@addFavor="handleAddFavor"
/>
</PixelModal>
<!-- Shop Overlay -->
<PixelModal
:isOpen="showShop"
@close="showShop = false"
title="SHOP"
>
<ShopOverlay
:playerGold="playerStats.gold || 0"
:inventory="inventory"
:shopItems="SHOP_ITEMS"
@buy="handleBuyItem"
@sell="handleSellItem"
/>
</PixelModal>
<!-- Adventure Selection Overlay -->
<PixelModal
:isOpen="showAdventureSelect"
@close="showAdventureSelect = false"
title="ADVENTURE"
>
<AdventureOverlay
:locations="ADVENTURE_LOCATIONS"
:playerStats="playerStats"
@selectLocation="handleStartAdventure"
@close="showAdventureSelect = false"
/>
</PixelModal>
<!-- Battle Result Modal (Custom Styling Modal) -->
<div v-if="showBattleResult" class="fixed inset-0 z-[110] flex items-center justify-center bg-black/80">
<div class="w-[500px] border-4 border-[#2ce8f4] bg-black p-1 shadow-[0_0_50px_#2ce8f4]">
<div class="border-2 border-[#2ce8f4] p-8 flex flex-col items-center gap-4">
<PartyPopper :size="48" class="text-[#99e550] animate-bounce" />
<h2 class="text-2xl text-[#99e550] font-bold tracking-widest">冒險完成 !</h2>
<div class="w-full border-t border-gray-700 my-2"></div>
<p class="text-gray-400 text-sm">這次沒有獲得任何獎勵...</p>
<button
@click="handleCloseBattleResult"
class="mt-6 border border-[#99e550] text-[#99e550] px-8 py-2 hover:bg-[#99e550] hover:text-black uppercase tracking-widest"
>
確定
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { PartyPopper } from 'lucide-vue-next';
import PlayerPanel from '~/components/pixel/PlayerPanel.vue';
import BattleArea from '~/components/pixel/BattleArea.vue';
import ActionArea from '~/components/pixel/ActionArea.vue';
import InfoPanel from '~/components/pixel/InfoPanel.vue';
import PixelModal from '~/components/pixel/PixelModal.vue';
import AchievementsOverlay from '~/components/pixel/AchievementsOverlay.vue';
import InventoryOverlay from '~/components/pixel/InventoryOverlay.vue';
import GodSystemOverlay from '~/components/pixel/GodSystemOverlay.vue';
import ShopOverlay from '~/components/pixel/ShopOverlay.vue';
import AdventureOverlay from '~/components/pixel/AdventureOverlay.vue';
import { PetSystem } from '../../core/pet-system.js';
import { TempleSystem } from '../../core/temple-system.js';
import { ApiService } from '../../core/api-service.js';
import {
ItemType,
Rarity,
EquipSlot,
DeityId,
ItemCategory
} from '~/types/pixel';
import type {
EntityStats,
Achievement,
Item,
Deity,
AdventureLocation
} from '~/types/pixel';
// --- SYSTEMS INITIALIZATION ---
const apiService = new ApiService({ useMock: true }); // Use mock for now
const petSystem = ref<PetSystem | null>(null);
const templeSystem = ref<TempleSystem | null>(null);
const initialized = ref(false);
// --- RESPONSIVE ---
const isDesktop = ref(true);
// Detect screen size
if (typeof window !== 'undefined') {
const updateScreenSize = () => {
isDesktop.value = window.innerWidth >= 768;
};
updateScreenSize();
window.addEventListener('resize', updateScreenSize);
onUnmounted(() => {
window.removeEventListener('resize', updateScreenSize);
});
}
// --- STATE ---
// Reactive state mapped from PetSystem
const systemState = ref<any>(null);
const allDeities = ref<Deity[]>([]);
const playerStats = computed<EntityStats>(() => {
if (!systemState.value) return {
name: "Loading...", class: "Egg", hp: 100, maxHp: 100, sp: 0, maxSp: 0, lvl: 1,
hunger: 100, maxHunger: 100, happiness: 100, maxHappiness: 100,
age: "0d 0h", generation: 1, height: "0 cm", weight: "0 g", gold: 0, fate: "Unknown",
godFavor: { name: "None", current: 0, max: 100 },
str: 0, int: 0, dex: 0, luck: 0, atk: 0, def: 0, spd: 0
};
const s = systemState.value;
const currentDeity = allDeities.value.find(d => d.id === s.currentDeityId);
return {
name: "Pet",
class: s.stage,
hp: Math.floor(s.health),
maxHp: 100,
sp: 0,
maxSp: 100,
lvl: 1,
hunger: Math.floor(s.hunger),
maxHunger: 100,
happiness: Math.floor(s.happiness),
maxHappiness: 100,
age: formatAge(s.ageSeconds),
generation: s.generation || 1,
height: `${s.height || 0} cm`,
weight: `${Math.floor(s.weight || 0)} g`,
gold: s.coins || 0,
fate: s.destiny?.name || "None",
godFavor: {
name: currentDeity?.name || "None",
current: s.deityFavors?.[s.currentDeityId] || 0,
max: 100
},
str: Math.floor(s.effectiveStr || s.str),
int: Math.floor(s.effectiveInt || s.int),
dex: Math.floor(s.effectiveDex || s.dex),
luck: Math.floor(s.effectiveLuck || s.luck),
atk: Math.floor(s.attack || 0),
def: Math.floor(s.defense || 0),
spd: Math.floor(s.speed || 0)
};
});
const inventory = computed<Item[]>(() => {
if (!systemState.value || !systemState.value.inventory) return [];
return systemState.value.inventory.map((i: any) => ({
...i,
icon: i.icon || 'circle',
statsDescription: i.description
}));
});
const deities = computed<Record<DeityId, Deity>>(() => {
const map: Record<string, Deity> = {};
allDeities.value.forEach(d => {
const favor = systemState.value?.deityFavors?.[d.id] || 0;
map[d.id] = { ...d, favor, maxFavor: 100 };
});
return map;
});
const currentDeity = computed(() => systemState.value?.currentDeityId || DeityId.Mazu);
// Modal States
const showAchievements = ref(false);
const showInventory = ref(false);
const showGodSystem = ref(false);
const showShop = ref(false);
const showAdventureSelect = ref(false);
const showBattleResult = ref(false);
// Battle State
const isFighting = ref(false);
const battleLogs = ref<string[]>([]);
// --- LIFECYCLE ---
onMounted(async () => {
petSystem.value = new PetSystem(apiService);
templeSystem.value = new TempleSystem(petSystem.value, apiService);
await petSystem.value.initialize();
await templeSystem.value.initialize();
systemState.value = petSystem.value.getState();
allDeities.value = templeSystem.value.getDeities();
petSystem.value.startTickLoop((newState) => {
systemState.value = newState;
});
initialized.value = true;
});
onUnmounted(() => {
if (petSystem.value) {
petSystem.value.stopTickLoop();
}
});
// --- HELPERS ---
const formatAge = (seconds: number) => {
if (!seconds) return '0h';
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
if (days > 0) return `${days}d ${hours}h`;
return `${hours}h`;
};
// --- HANDLERS ---
const handleStartAdventure = (location: AdventureLocation) => {
showAdventureSelect.value = false;
if (petSystem.value) {
petSystem.value.updateState({
hunger: Math.max(0, systemState.value.hunger - location.costHunger),
coins: Math.max(0, systemState.value.coins - location.costGold)
});
}
isFighting.value = true;
battleLogs.value = [`Entered ${location.name}...`, `Encountered ${location.enemyName}!`];
let turn = 1;
const interval = setInterval(() => {
if (turn > 5) {
clearInterval(interval);
battleLogs.value.push("Victory!", "Obtained 10 EXP!");
setTimeout(() => {
isFighting.value = false;
showBattleResult.value = true;
}, 1500);
return;
}
const isPlayerTurn = turn % 2 !== 0;
if (isPlayerTurn) {
battleLogs.value.push(`You used Attack! Dealt ${Math.floor(Math.random() * 20) + 10} damage.`);
} else {
battleLogs.value.push(`${location.enemyName} attacked! You took ${Math.floor(Math.random() * 10)} damage.`);
}
turn++;
}, 1000);
};
const handleCloseBattleResult = () => {
showBattleResult.value = false;
battleLogs.value = [];
};
const handleEquip = async (itemId: string, asAppearance: boolean) => {
console.log("Equip not fully implemented in core yet", itemId);
};
const handleUnequip = async (slot: EquipSlot, asAppearance: boolean) => {
console.log("Unequip not fully implemented in core yet", slot);
};
const handleUseItem = async (itemId: string) => {
console.log("Use item not fully implemented in core yet", itemId);
};
const handleDeleteItem = async (itemId: string) => {
console.log("Delete item not fully implemented in core yet", itemId);
};
const handleSwitchDeity = async (id: DeityId) => {
if (templeSystem.value) {
await templeSystem.value.switchDeity(id);
systemState.value = petSystem.value?.getState();
}
};
const handleAddFavor = async (amount: number) => {
if (templeSystem.value) {
await templeSystem.value.pray();
systemState.value = petSystem.value?.getState();
}
};
const handleBuyItem = async (item: Item) => {
if (petSystem.value && systemState.value.coins >= item.price) {
const newCoins = systemState.value.coins - item.price;
const newInventory = [...(systemState.value.inventory || []), { ...item, id: `buy-${Date.now()}` }];
await petSystem.value.updateState({ coins: newCoins, inventory: newInventory });
systemState.value = petSystem.value.getState();
} else {
alert("Not enough gold!");
}
};
const handleSellItem = async (item: Item) => {
if (petSystem.value) {
const sellPrice = Math.floor(item.price / 2);
const newCoins = systemState.value.coins + sellPrice;
const newInventory = systemState.value.inventory.filter((i: any) => i.id !== item.id);
await petSystem.value.updateState({ coins: newCoins, inventory: newInventory });
systemState.value = petSystem.value.getState();
}
};
// --- MOCK DATA FOR STATIC CONTENT ---
const ADVENTURE_LOCATIONS: AdventureLocation[] = [
{ id: '1', name: '自家後院', description: '安全的新手探險地,偶爾會有小蟲子。', costHunger: 5, costGold: 5, difficulty: 'Easy', enemyName: '野蟲' },
{ id: '2', name: '附近的公園', description: '熱鬧的公園,但也潛藏著流浪動物的威脅。', costHunger: 15, costGold: 10, reqStats: { str: 20 }, difficulty: 'Medium', enemyName: '流浪貓' },
{ id: '3', name: '神秘森林', description: '危險的未知區域,只有強者才能生存。', costHunger: 30, costGold: 20, reqStats: { str: 50, int: 30 }, difficulty: 'Hard', enemyName: '樹妖' }
];
const ACHIEVEMENTS_DATA: Achievement[] = [
{ id: '1', title: 'First Step', description: 'Pet age reaches 1 hour', reward: 'STR Growth +5% INT Growth +5%', progress: 100, unlocked: true, icon: 'baby', color: '#ffe762' },
{ id: '2', title: 'One Day Plan', description: 'Pet age reaches 1 day', reward: 'STR/INT/DEX Growth +10% LUCK +2', progress: 100, unlocked: true, icon: 'calendar', color: '#ffe762' },
];
const SHOP_ITEMS: Item[] = [
{ id: 's1', name: 'Fortune Cookie', type: ItemType.Consumable, category: ItemCategory.Food, price: 10, rarity: Rarity.Common, description: 'A crisp cookie with a fortune inside.', statsDescription: 'Happiness +5', icon: 'cookie' },
{ id: 's2', name: 'Tuna Can', type: ItemType.Consumable, category: ItemCategory.Food, price: 30, rarity: Rarity.Common, description: 'High quality tuna. Cats love it.', statsDescription: 'Hunger -50', icon: 'fish' },
{ id: 's3', name: 'Premium Food', type: ItemType.Consumable, category: ItemCategory.Food, price: 50, rarity: Rarity.Excellent, description: 'Gourmet pet food.', statsDescription: 'Hunger -100 Happiness +10', icon: 'star' },
{ id: 's4', name: 'Magic Wand', type: ItemType.Equipment, category: ItemCategory.Toy, price: 150, rarity: Rarity.Rare, description: 'A toy wand that sparkles.', statsDescription: 'Happiness Regen', slot: EquipSlot.Weapon, icon: 'wand' },
{ id: 's5', name: 'Ball', type: ItemType.Equipment, category: ItemCategory.Toy, price: 20, rarity: Rarity.Common, description: 'A bouncy ball.', statsDescription: 'Play +10', slot: EquipSlot.Weapon, icon: 'ball' },
{ id: 's6', name: 'Lucky Coin', type: ItemType.Equipment, category: ItemCategory.Accessory, price: 500, rarity: Rarity.Epic, description: 'Increases luck significantly.', statsDescription: 'LCK +10', slot: EquipSlot.Accessory, icon: 'coin' },
{ id: 's7', name: 'Health Elixir', type: ItemType.Consumable, category: ItemCategory.Medicine, price: 100, rarity: Rarity.Rare, description: 'Fully restores health.', statsDescription: 'HP Full', icon: 'potion' },
];
</script>

View File

@ -1,180 +0,0 @@
export enum StatType {
HP = 'HP',
SP = 'SP',
ATK = 'ATK',
DEF = 'DEF',
SPD = 'SPD',
LUCK = 'LCK'
}
export interface EntityStats {
hp: number;
maxHp: number;
sp: number; // Re-purposed for 'Hunger' or general resource if needed
maxSp: number;
lvl: number;
name: string;
class: string;
// New Pet Fields
hunger?: number;
maxHunger?: number;
happiness?: number;
maxHappiness?: number;
age?: string;
generation?: number;
height?: string;
weight?: string;
gold?: number;
fate?: string; // e.g. "Resource Recycling Grandma"
godFavor?: {
name: string;
current: number;
max: number;
};
// Detailed Stats
str?: number;
int?: number;
dex?: number;
luck?: number;
atk?: number;
def?: number;
spd?: number;
}
export interface ActionItem {
id: string;
name: string;
iconName: string; // Mapping to Lucide icon
cooldown: number;
cost: number;
}
export interface EquipmentSlot {
slot: string;
item: string;
}
export interface Achievement {
id: string;
title: string;
description: string;
reward?: string;
progress: number; // 0 to 100 percentage
currentValue?: number;
maxValue?: number;
unlocked: boolean;
icon: string; // Helper to map to Lucide icon in component
color?: string;
}
// --- Inventory System Types ---
export enum Rarity {
Common = 'Common',
Excellent = 'Excellent',
Rare = 'Rare',
Epic = 'Epic',
Legendary = 'Legendary'
}
export enum ItemType {
Equipment = 'Equipment',
Consumable = 'Consumable'
}
export enum ItemCategory {
Food = 'Food',
Medicine = 'Medicine',
Equipment = 'Equipment',
Toy = 'Toy',
Accessory = 'Accessory',
Misc = 'Misc'
}
export enum EquipSlot {
Weapon = 'Weapon',
Armor = 'Armor',
Hat = 'Hat',
Accessory = 'Accessory',
Charm = 'Charm',
Special = 'Special'
}
export interface Item {
id: string;
name: string;
type: ItemType;
category?: ItemCategory; // New field for Shop filters
price: number; // New field for Shop
slot?: EquipSlot; // Only for Equipment
rarity: Rarity;
description: string;
statsDescription?: string; // e.g. "DEF +8 MaxHP +10"
effects?: string[];
icon: string;
quantity?: number; // For consumables
// State helpers
isEquipped?: boolean;
isAppearance?: boolean; // If true, it's in the appearance slot
}
// --- God System Types ---
export enum DeityId {
Mazu = 'Mazu', // 媽祖
EarthGod = 'EarthGod', // 土地公
Matchmaker = 'Matchmaker', // 月老
Wenchang = 'Wenchang' // 文昌
}
export enum JiaobeiResult {
Saint = 'Saint', // 聖杯 (One up, one down) - YES
Smile = 'Smile', // 笑杯 (Two flat faces up) - LAUGH/MAYBE
Cry = 'Cry', // 陰杯 (Two round faces up) - NO
None = 'None'
}
export enum LotPhase {
Idle = 'Idle',
Drawing = 'Drawing', // Shaking the cylinder
PendingVerify = 'PendingVerify', // Lot drawn, needs 3 saint cups
Verifying = 'Verifying', // Tossing blocks
Success = 'Success', // Got 3 saint cups
Failed = 'Failed' // Failed mid-way
}
export interface Deity {
id: DeityId;
name: string;
title: string;
description: string;
favor: number;
maxFavor: number;
colors: {
skin: string;
hair: string;
outfit: string;
accessory: string;
};
}
// --- Adventure System Types ---
export interface AdventureLocation {
id: string;
name: string;
description: string;
costHunger: number;
costGold: number;
reqStats?: {
str?: number;
int?: number;
};
difficulty: 'Easy' | 'Medium' | 'Hard';
enemyName: string;
}

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

@ -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

@ -11,28 +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',
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: 548 KiB

View File

@ -1,38 +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: {
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'
}
}
}
}
}