good version

This commit is contained in:
王性驊 2025-11-26 23:31:46 +08:00
parent 8def70de92
commit 086a7b796a
12 changed files with 1383 additions and 299 deletions

View File

@ -1,25 +1,22 @@
<template> <template>
<div class="flex flex-col gap-4 h-full"> <div class="flex flex-col gap-4 h-full">
<!-- Header Stats --> <!-- Stats Bar -->
<div class="flex justify-between items-center bg-[#231533] p-3 border-2 border-[#4a3b5e]"> <div class="flex justify-between items-center bg-[#231533] p-2 border-2 border-[#4a3b5e] text-xs font-mono">
<div class="flex gap-4"> <div class="flex gap-4">
<span class="text-[#99e550] tracking-widest uppercase">Unlocked: {{ unlocked }}/{{ total }}</span> <span class="text-[#e0d8f0]">總成就: <span class="text-[#f6b26b]">{{ total }}</span></span>
<span class="text-[#e0d8f0]">已解鎖: <span class="text-[#99e550]">{{ unlocked }}</span></span>
</div> </div>
<div class="w-1/3 flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-[#99e550] tracking-widest uppercase text-sm whitespace-nowrap">Progress: {{ percentage }}%</span> <span class="text-[#e0d8f0]">達成率: <span class="text-[#2ce8f4]">{{ percentage }}%</span></span>
<RetroProgressBar :progress="percentage" color="#99e550" /> <div class="w-20">
<RetroProgressBar :progress="percentage" color="#2ce8f4" height="6px" />
</div>
</div> </div>
</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 --> <!-- 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 class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 overflow-y-auto pb-4 custom-scrollbar">
<div <div
v-for="achievement in achievements" v-for="achievement in achievements"
:key="achievement.id" :key="achievement.id"
@ -28,33 +25,37 @@
> >
<!-- Header: Icon + Title --> <!-- Header: Icon + Title -->
<div class="flex items-start gap-3"> <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]'"> <div class="p-2 rounded-sm border-2 flex items-center justify-center w-10 h-10" :class="achievement.unlocked ? 'border-[#99e550] bg-[#4b692f]/20' : 'border-[#4a3b5e] bg-[#2b193f]'">
<component :is="ICON_MAP[achievement.icon] || Trophy" :size="24" :color="achievement.unlocked ? achievement.color || '#ffe762' : '#8f80a0'" /> <component :is="getIcon(achievement.icon)" :size="20" :color="achievement.unlocked ? '#99e550' : '#8f80a0'" />
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<h4 class="font-bold tracking-wide leading-none mb-1" :class="achievement.unlocked ? 'text-[#2ce8f4]' : 'text-[#8f80a0]'"> <h4 class="font-bold tracking-wide leading-none mb-1" :class="achievement.unlocked ? 'text-[#2ce8f4]' : 'text-[#8f80a0]'">
{{ achievement.title }} {{ achievement.name }}
</h4> </h4>
<span v-if="achievement.unlocked" class="text-[10px] text-[#99e550] uppercase tracking-widest">Completed</span> <span v-if="achievement.unlocked" class="text-[10px] text-[#99e550] uppercase tracking-widest">已完成</span>
<span v-else class="text-[10px] text-[#8f80a0] uppercase tracking-widest flex items-center gap-1"> <span v-else class="text-[10px] text-[#8f80a0] uppercase tracking-widest flex items-center gap-1">
<Lock :size="10" /> Locked <Lock :size="10" /> 未解鎖
</span> </span>
</div> </div>
</div> </div>
<!-- Description --> <!-- Description -->
<p class="text-xs text-[#e0d8f0] flex-grow leading-tight"> <p class="text-xs text-[#e0d8f0] flex-grow leading-tight mt-1">
{{ achievement.description }} {{ achievement.description }}
</p> </p>
<!-- Reward Section (if exists) --> <!-- Reward Section (if exists) -->
<div v-if="achievement.reward" class="text-[10px] text-[#99e550]"> <div v-if="achievement.reward && achievement.reward.buffs" class="text-[10px] text-[#99e550] mt-2 border-t border-[#4a3b5e] pt-1">
<span class="text-[#99e550] opacity-70">Reward: </span> <span class="text-[#99e550] opacity-70">獎勵: </span>
{{ achievement.reward }} <div class="flex flex-wrap gap-1 mt-0.5">
<span v-for="(value, key) in achievement.reward.buffs" :key="key" class="bg-[#2b193f] px-1 rounded border border-[#4a3b5e]">
{{ formatBuffKey(key) }}+{{ formatValue(value) }}
</span>
</div>
</div> </div>
<!-- Progress Bar (if incomplete) --> <!-- Progress Bar (if incomplete) -->
<div v-if="!achievement.unlocked && achievement.maxValue" class="mt-auto"> <div v-if="!achievement.unlocked && achievement.maxValue" class="mt-auto pt-2">
<div class="flex justify-between text-[9px] text-[#8f80a0] mb-0.5"> <div class="flex justify-between text-[9px] text-[#8f80a0] mb-0.5">
<span>{{ achievement.currentValue }} / {{ achievement.maxValue }}</span> <span>{{ achievement.currentValue }} / {{ achievement.maxValue }}</span>
<span>{{ achievement.progress }}%</span> <span>{{ achievement.progress }}%</span>
@ -75,9 +76,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; 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 {
import PixelFrame from './PixelFrame.vue'; CheckCircle2, Lock, Trophy, Baby, CalendarDays, Egg, Sprout, Cake, Star, Diamond,
Milk, Utensils, Gamepad2, Sparkles, BookOpen, Search, Leaf, Dumbbell, Brush, Pill,
HandHeart, Heart, Moon
} from 'lucide-vue-next';
import RetroProgressBar from './RetroProgressBar.vue'; import RetroProgressBar from './RetroProgressBar.vue';
import { formatBuffKey } from '../../utils/formatters.js';
type Achievement = any; type Achievement = any;
@ -88,27 +93,42 @@ interface Props {
const props = defineProps<Props>(); const props = defineProps<Props>();
const ICON_MAP: Record<string, any> = { const ICON_MAP: Record<string, any> = {
baby: Baby, '👶': Baby,
calendar: CalendarDays, '📅': CalendarDays,
egg: Egg, '🥚': Egg,
sprout: Sprout, '🌱': Sprout,
cake: Cake, '🎂': Cake,
star: Star, '⭐': Star,
diamond: Diamond, '💎': Diamond,
milk: Milk, '🍼': Milk,
utensils: Utensils, '🍽️': Utensils,
gamepad: Gamepad2, '🎮': Gamepad2,
sparkles: Sparkles, '🧹': Brush,
book: BookOpen, '💊': Pill,
search: Search, '🙏': HandHeart,
leaf: Leaf, '🕯️': Sparkles,
dumbbell: Dumbbell, '👼': Heart,
brush: Brush, '🌟': Star,
pill: Pill, '🎴': BookOpen,
trophy: Trophy, '🔍': Search,
'🍀': Leaf,
'💪': Dumbbell,
'✨': Sparkles,
'🌙': Moon
};
const getIcon = (iconKey: string) => {
return ICON_MAP[iconKey] || Trophy;
};
const formatValue = (value: number) => {
if (value < 1 && value > 0) {
return Math.round(value * 100) + '%';
}
return value;
}; };
const total = computed(() => props.achievements.length); const total = computed(() => props.achievements.length);
const unlocked = computed(() => props.achievements.filter(a => a.unlocked).length); const unlocked = computed(() => props.achievements.filter(a => a.unlocked).length);
const percentage = computed(() => Math.round((unlocked.value / total.value) * 100)); const percentage = computed(() => total.value > 0 ? Math.round((unlocked.value / total.value) * 100) : 0);
</script> </script>

View File

@ -49,7 +49,7 @@ interface Props {
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
const emit = defineEmits(['feed', 'play', 'train', 'puzzle', 'clean', 'heal', 'openInventory', 'openGodSystem', 'openShop', 'openAdventure', 'toggleSleep']); const emit = defineEmits(['feed', 'play', 'train', 'puzzle', 'clean', 'heal', 'openInventory', 'openGodSystem', 'openShop', 'openAdventure', 'toggleSleep', 'debugAddItems']);
const BASE_ACTIONS = [ const BASE_ACTIONS = [
{ {
@ -108,6 +108,12 @@ const BASE_ACTIONS = [
label: '商店', label: '商店',
color: '#ffa500', color: '#ffa500',
pixelPath: 'M3 2H7V3H3V2ZM2 3H8V7H2V3ZM3 7H7V8H3V7ZM4 4H6V5H4V4Z' pixelPath: 'M3 2H7V3H3V2ZM2 3H8V7H2V3ZM3 7H7V8H3V7ZM4 4H6V5H4V4Z'
},
{
id: 'debug',
label: '測試',
color: '#ff00ff',
pixelPath: 'M2 2H8V3H2V2ZM2 7H8V8H2V7ZM2 3H3V7H2V3ZM7 3H8V7H7V3ZM4 4H6V6H4V4Z' // Box with dot
} }
]; ];
@ -147,5 +153,6 @@ const handleActionClick = (id: string) => {
else if (id === 'sleep' || id === 'wake') emit('toggleSleep'); else if (id === 'sleep' || id === 'wake') emit('toggleSleep');
else if (id === 'pray') emit('openGodSystem'); else if (id === 'pray') emit('openGodSystem');
else if (id === 'shop') emit('openShop'); else if (id === 'shop') emit('openShop');
else if (id === 'debug') emit('debugAddItems');
}; };
</script> </script>

View File

@ -1,179 +1,226 @@
<template> <template>
<div class="flex flex-col h-full gap-2"> <div class="flex flex-col h-full gap-2">
<!-- 1. Rarity Legend --> <!-- 1. 已装备装备栏 -->
<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 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"> <div v-for="(slotConfig, key) in EQUIPMENT_SLOTS" :key="key" class="border border-[#4a3b5e] bg-[#0f0816] p-2 flex flex-col gap-2">
<!-- Slot Header --> <!-- 装备槽标题 -->
<div class="flex items-center gap-2 mb-1 justify-center border-b border-[#2b193f] pb-1"> <div class="flex items-center gap-2 justify-center border-b border-[#2b193f] pb-1">
<component :is="SLOT_ICONS[slot]" :size="14" class="text-[#8f80a0]" /> <component :is="SLOT_ICONS[key]" :size="14" class="text-[#8f80a0]" />
<span class="text-[#2ce8f4] text-xs font-bold uppercase tracking-wider">{{ slot }}</span> <span class="text-[#2ce8f4] text-xs font-bold uppercase">{{ slotConfig.name }}</span>
</div> </div>
<!-- Actual Slot --> <!-- 实际装备槽 -->
<div <div class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center">
class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center cursor-pointer hover:border-[#9fd75b] group" <span class="text-[9px] text-[#8f80a0] mb-1">装备</span>
@click="getEquippedItem(slot, false) && setSelectedItemId(getEquippedItem(slot, false)?.id)" <div v-if="getEquippedItem(key, false)" class="flex flex-col items-center gap-1">
> <div class="relative w-8 h-8">
<span class="text-[9px] text-[#8f80a0] mb-0.5">ACTUAL</span> <PixelItemIcon :category="getEquippedItem(key, false)!.category" :color="ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color" />
<span v-if="getEquippedItem(slot, false)" class="text-xs text-center" :style="{ color: RARITY_COLORS[getEquippedItem(slot, false)!.rarity] }">{{ getEquippedItem(slot, false)!.name }}</span> </div>
<span v-else class="text-[10px] text-[#4a3b5e]">Empty</span> <span class="text-xs text-center truncate w-full px-1 font-bold" :style="{ color: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color }">{{ getEquippedItem(key, false)!.name }}</span>
</div>
<span v-else class="text-[10px] text-[#4a3b5e] z-10 opacity-50"></span>
</div> </div>
<!-- Appearance Slot --> <!-- 外观槽 -->
<div <div class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center">
class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center cursor-pointer hover:border-[#d584fb] group" <span class="text-[9px] text-[#8f80a0] mb-1">外觀</span>
@click="getEquippedItem(slot, true) && setSelectedItemId(getEquippedItem(slot, true)?.id)" <div v-if="getEquippedItem(key, true)" class="flex flex-col items-center gap-1">
> <div class="relative w-8 h-8">
<span class="text-[9px] text-[#8f80a0] mb-0.5">COSMETIC</span> <PixelItemIcon :category="getEquippedItem(key, true)!.category" :color="ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color" />
<span v-if="getEquippedItem(slot, true)" class="text-xs text-center" :style="{ color: RARITY_COLORS[getEquippedItem(slot, true)!.rarity] }">{{ getEquippedItem(slot, true)!.name }}</span> </div>
<span v-else class="text-[10px] text-[#4a3b5e]">Empty</span> <span class="text-xs text-center truncate w-full px-1 font-bold" :style="{ color: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color }">{{ getEquippedItem(key, true)!.name }}</span>
</div>
<span v-else class="text-[10px] text-[#4a3b5e] z-10 opacity-50"></span>
</div> </div>
</div> </div>
</div> </div>
<!-- 3. Backpack Section --> <!-- 2. 背包区域 - 全宽 + 固定高度 + 滚动 -->
<div class="flex-grow flex flex-col md:flex-row gap-2 overflow-hidden mt-2"> <div class="flex-grow overflow-hidden mt-2">
<PixelFrame class="h-full flex flex-col bg-[#1b1026]" :title="`背包 (${items.filter(i => !i.isEquipped).length})`">
<!-- Item Grid --> <div class="overflow-y-auto p-3 custom-scrollbar" style="max-height: calc(100vh - 400px);">
<PixelFrame class="flex-grow flex flex-col bg-[#1b1026]" :title="`Backpack (${items.filter(i => !i.isEquipped).length})`"> <div class="grid grid-cols-6 gap-2">
<div class="flex-grow overflow-y-auto p-1 custom-scrollbar"> <!-- 30个格子6x5 -->
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2"> <div v-for="index in 30" :key="index" class="relative aspect-square">
<button
v-for="item in items.filter(i => !i.isEquipped)" <!-- 有物品显示物品 + 悬浮卡片 -->
:key="item.id" <template v-if="getInventorySlotItem(index - 1)">
@click="setSelectedItemId(item.id)" <button class="w-full h-full p-2 flex flex-col items-center justify-center gap-1 border-2 transition-all group rounded-sm border-[#4a3b5e] hover:border-[#8f80a0] hover:bg-[#321e4a] bg-[#2b193f]">
class="relative p-2 flex flex-col items-center justify-center gap-1 min-h-[80px] border-2 transition-all group bg-[#2b193f]" <div class="relative w-10 h-10">
:class="selectedItemId === item.id ? 'border-white bg-[#3d2459]' : 'border-[#4a3b5e] hover:border-[#8f80a0]'" <PixelItemIcon
> :category="getInventorySlotItem(index - 1).category"
<div class="relative"> :color="ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color"
<!-- 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]" /> <div
<Crown v-else-if="item.slot === EquipSlot.Hat" :color="RARITY_COLORS[item.rarity]" /> v-if="['rare', 'epic', 'legendary'].includes(getInventorySlotItem(index - 1).rarity)"
<Gem v-else-if="item.slot === EquipSlot.Accessory" :color="RARITY_COLORS[item.rarity]" /> class="absolute inset-0 rounded-full opacity-40 blur-md pointer-events-none"
<Sparkles v-else-if="item.slot === EquipSlot.Charm" :color="RARITY_COLORS[item.rarity]" /> :style="{ backgroundColor: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color }"
<Star v-else :color="RARITY_COLORS[item.rarity]" /> ></div>
</template>
<template v-else> <span
<Zap v-if="item.name.includes('Potion')" :color="RARITY_COLORS[item.rarity]" /> v-if="getInventorySlotItem(index - 1).quantity && getInventorySlotItem(index - 1).quantity > 1"
<Heart v-else :color="RARITY_COLORS[item.rarity]" /> class="absolute -bottom-1 -right-1 text-[9px] bg-black text-white px-1 border border-[#4a3b5e] rounded-sm z-10 shadow-sm font-mono"
</template> >{{ getInventorySlotItem(index - 1).quantity }}</span>
</div>
<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>
<span
class="text-[10px] text-center leading-tight line-clamp-2 w-full font-medium relative z-10"
:style="{
color: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color,
textShadow: ['rare', 'epic', 'legendary'].includes(getInventorySlotItem(index - 1).rarity)
? `0 0 3px ${ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color}80`
: 'none'
}"
>{{ getInventorySlotItem(index - 1).name }}</span>
<!-- 详细悬浮卡片 -->
<div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-4 py-3 bg-[#0f0816] border-2 border-[#4a3b5e] rounded-sm shadow-xl opacity-0 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-auto transition-opacity z-50 w-72 text-left max-h-[400px] overflow-y-auto custom-scrollbar">
<!-- 物品头部 -->
<div class="flex gap-3 mb-3 pb-3 border-b border-[#4a3b5e]">
<div class="w-16 h-16 bg-[#1b1026] border-2 rounded flex items-center justify-center p-2 flex-shrink-0"
:style="{ borderColor: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color }">
<PixelItemIcon
:category="getInventorySlotItem(index - 1).category"
:color="ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color"
/>
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-bold mb-1"
:style="{ color: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color }">
{{ getInventorySlotItem(index - 1).name }}
</div>
<div class="flex gap-2 text-[10px] flex-wrap">
<span class="px-2 py-0.5 bg-[#1b1026] border border-[#4a3b5e] rounded text-[#8f80a0]">
{{ getItemTypeName(getInventorySlotItem(index - 1).type) }}
</span>
<span class="px-2 py-0.5 border rounded font-bold"
:style="{
borderColor: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color,
color: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color
}">
{{ ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.name }}
</span>
</div>
</div>
</div>
<!-- 描述 -->
<p class="text-[11px] text-[#e0d8f0] italic mb-3 leading-relaxed">
"{{ getInventorySlotItem(index - 1).description }}"
</p>
<!-- 效果 -->
<div v-if="getInventorySlotItem(index - 1).effects" class="mb-3 space-y-1">
<div class="text-[10px] text-[#99e550] font-bold mb-1">效果</div>
<div v-if="getInventorySlotItem(index - 1).effects.flat" class="space-y-1">
<div v-for="(val, key) in getInventorySlotItem(index - 1).effects.flat" :key="key"
class="text-[10px] text-[#9fd75b] flex justify-between border-b border-[#2b193f] border-dashed pb-1">
<span class="text-[#8f80a0]">{{ formatBuffKey(key) }}</span>
<span class="font-mono font-bold">+{{ val }}</span>
</div>
</div>
<div v-if="getInventorySlotItem(index - 1).effects.percent" class="space-y-1 mt-1">
<div v-for="(val, key) in getInventorySlotItem(index - 1).effects.percent" :key="key"
class="text-[10px] text-[#2ce8f4] flex justify-between border-b border-[#2b193f] border-dashed pb-1">
<span class="text-[#8f80a0]">{{ formatBuffKey(key) }}</span>
<span class="font-mono font-bold">+{{ (val * 100).toFixed(0) }}%</span>
</div>
</div>
</div>
<!-- 耐久度 -->
<div v-if="getInventorySlotItem(index - 1).maxDurability && getInventorySlotItem(index - 1).maxDurability !== Infinity" class="mb-3">
<div class="flex justify-between text-[10px] text-[#8f80a0] mb-1">
<span>耐久度</span>
<span class="font-mono">{{ getInventorySlotItem(index - 1).durability }} / {{ getInventorySlotItem(index - 1).maxDurability }}</span>
</div>
<div class="h-1.5 w-full bg-[#1b1026] border border-[#4a3b5e] rounded-full overflow-hidden">
<div class="h-full bg-[#f6b26b] transition-all"
:style="{ width: `${(getInventorySlotItem(index - 1).durability / getInventorySlotItem(index - 1).maxDurability) * 100}%` }">
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="space-y-2 pt-2 border-t border-[#4a3b5e]">
<!-- 装备按钮 -->
<button
v-if="getInventorySlotItem(index - 1).slot && getInventorySlotItem(index - 1).type !== ItemType.Appearance"
@click.stop="$emit('equip', getInventorySlotItem(index - 1).id, false)"
class="w-full px-3 py-2 bg-[#9fd75b] text-[#1b1026] rounded text-xs font-bold hover:bg-[#b5e87b] transition-colors"
>
裝備
</button>
<!-- 外观按钮 -->
<button
v-if="getInventorySlotItem(index - 1).type === ItemType.Appearance"
@click.stop="$emit('equip', getInventorySlotItem(index - 1).id, true)"
class="w-full px-3 py-2 bg-[#d584fb] text-[#1b1026] rounded text-xs font-bold hover:bg-[#e5a4ff] transition-colors"
>
裝備外觀
</button>
<!-- 使用按钮 -->
<button
v-if="getInventorySlotItem(index - 1).type === ItemType.Consumable"
@click.stop="$emit('use', getInventorySlotItem(index - 1).id)"
class="w-full px-3 py-2 bg-[#2ce8f4] text-[#1b1026] rounded text-xs font-bold hover:bg-[#5cf4ff] transition-colors"
>
使用物品
</button>
<!-- 修理按钮 -->
<button
v-if="getInventorySlotItem(index - 1).maxDurability && getInventorySlotItem(index - 1).maxDurability !== Infinity && getInventorySlotItem(index - 1).durability < getInventorySlotItem(index - 1).maxDurability"
@click.stop="$emit('repair', getInventorySlotItem(index - 1).id)"
class="w-full px-3 py-2 bg-[#f6b26b] text-[#1b1026] rounded text-xs font-bold hover:bg-[#ffc68b] transition-colors flex items-center justify-center gap-2"
>
<Hammer :size="12" />
修理 ({{ Math.ceil((getInventorySlotItem(index - 1).maxDurability - getInventorySlotItem(index - 1).durability) * 0.5) }} G)
</button>
<!-- 删除按钮 -->
<button
@click.stop="$emit('delete', getInventorySlotItem(index - 1).id)"
class="w-full px-3 py-2 bg-[#d95763] text-white rounded text-xs font-bold hover:bg-[#ff6b7a] transition-colors flex items-center justify-center gap-2"
>
<Trash2 :size="12" />
丟棄
</button>
</div>
</div>
</button>
</template>
<!-- 空格子占位符 -->
<div
v-else
class="w-full h-full border-2 border-[#2b193f] bg-[#150c1f] rounded-sm flex items-center justify-center opacity-30"
>
<div class="w-8 h-8 border border-[#2b193f] rounded opacity-20"></div>
</div> </div>
<span class="text-[10px] text-center leading-tight line-clamp-2" :style="{ color: RARITY_COLORS[item.rarity] }"> </div>
{{ item.name }}
</span>
</button>
</div> </div>
</div> </div>
</PixelFrame> </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>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { Sword, Shield, Crown, Gem, Sparkles, Star, Shirt, HelpCircle, Trash2, Zap, Heart, Package, X } from 'lucide-vue-next'; import { Sword, Shield, Crown, Gem, Sparkles, Star, HelpCircle, Trash2, X, Hammer } from 'lucide-vue-next';
import PixelFrame from './PixelFrame.vue'; import PixelFrame from './PixelFrame.vue';
import PixelButton from './PixelButton.vue'; import PixelButton from './PixelButton.vue';
import PixelItemIcon from './PixelItemIcon.vue';
import { formatBuffKey } from '../../utils/formatters.js';
import { ITEM_TYPE, ITEM_RARITY, EQUIPMENT_SLOTS } from '../../../../data/items.js'; import { ITEM_TYPE, ITEM_RARITY, EQUIPMENT_SLOTS, ITEM_TYPES } from '../../../../data/items.js';
// Use any types since we're using data directly // Use any types since we're using data directly
type Item = any; type Item = any;
@ -183,12 +230,16 @@ interface Props {
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
defineEmits(['equip', 'unequip', 'use', 'delete']); defineEmits(['equip', 'unequip', 'use', 'delete', 'repair']);
const selectedItemId = ref<string | null>(null); const selectedItemId = ref<string | null>(null);
const selectedItem = computed(() => props.items.find(i => i.id === selectedItemId.value)); const selectedItem = computed(() => props.items.find(i => i.id === selectedItemId.value));
const setSelectedItemId = (id: string | undefined) => {
if (id) selectedItemId.value = id;
};
// Map data constants to local helpers for template compatibility // Map data constants to local helpers for template compatibility
const ItemType = { const ItemType = {
Equipment: ITEM_TYPE.EQUIPMENT, Equipment: ITEM_TYPE.EQUIPMENT,
@ -198,41 +249,25 @@ const ItemType = {
Appearance: ITEM_TYPE.APPEARANCE Appearance: ITEM_TYPE.APPEARANCE
}; };
const Rarity = {
Common: 'common',
Excellent: 'uncommon',
Rare: 'rare',
Epic: 'epic',
Legendary: 'legendary'
};
const EquipSlot = {
Weapon: 'weapon',
Armor: 'armor',
Hat: 'hat',
Accessory: 'accessory',
Charm: 'talisman',
Special: 'special'
};
const RARITY_COLORS: Record<string, 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<string, any> = { const SLOT_ICONS: Record<string, any> = {
[EquipSlot.Weapon]: Sword, weapon: Sword,
[EquipSlot.Armor]: Shield, armor: Shield,
[EquipSlot.Hat]: Crown, hat: Crown,
[EquipSlot.Accessory]: Gem, accessory: Gem,
[EquipSlot.Charm]: Sparkles, talisman: Sparkles,
[EquipSlot.Special]: Star, special: Star,
}; };
const getEquippedItem = (slot: string, isAppearance: boolean) => { const getEquippedItem = (slot: string, isAppearance: boolean) => {
return props.items.find(i => i.isEquipped && i.slot === slot && !!i.isAppearance === isAppearance); return props.items.find(i => i.isEquipped && i.slot === slot && !!i.isAppearance === isAppearance);
}; };
const getInventorySlotItem = (index: number) => {
const unequippedItems = props.items.filter(i => !i.isEquipped);
return unequippedItems[index] || null;
};
const getItemTypeName = (type: string) => {
return ITEM_TYPES[type]?.name || type;
};
</script> </script>

View File

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

View File

@ -4,7 +4,30 @@
<div class="text-center"> <div class="text-center">
<h2 class="text-[#99e550] text-xl font-bold mb-2 tracking-widest">歡迎來到電子雞世界</h2> <h2 class="text-[#99e550] text-xl font-bold mb-2 tracking-widest">歡迎來到電子雞世界</h2>
<p class="text-[#8f80a0] text-sm">請為您的新夥伴取個名字</p> <p class="text-[#8f80a0] text-sm">請選擇您的夥伴並取個名字</p>
</div>
<!-- Species Selection -->
<div class="flex gap-4 w-full justify-center">
<button
@click="species = 'cat'"
class="flex-1 p-4 border-2 flex flex-col items-center gap-2 transition-all relative group"
:class="species === 'cat' ? 'border-[#99e550] bg-[#2b193f]' : 'border-[#4a3b5e] bg-[#0f0816] hover:bg-[#1b1026]'"
>
<div class="text-2xl">🐱</div>
<span class="font-bold font-mono" :class="species === 'cat' ? 'text-[#99e550]' : 'text-[#8f80a0]'">貓咪 (Cat)</span>
<div v-if="species === 'cat'" class="absolute top-1 right-1 w-2 h-2 bg-[#99e550]"></div>
</button>
<button
@click="species = 'dog'"
class="flex-1 p-4 border-2 flex flex-col items-center gap-2 transition-all relative group"
:class="species === 'dog' ? 'border-[#99e550] bg-[#2b193f]' : 'border-[#4a3b5e] bg-[#0f0816] hover:bg-[#1b1026]'"
>
<div class="text-2xl">🐶</div>
<span class="font-bold font-mono" :class="species === 'dog' ? 'text-[#99e550]' : 'text-[#8f80a0]'">狗狗 (Dog)</span>
<div v-if="species === 'dog'" class="absolute top-1 right-1 w-2 h-2 bg-[#99e550]"></div>
</button>
</div> </div>
<div class="w-full"> <div class="w-full">
@ -43,12 +66,13 @@ import PixelButton from './PixelButton.vue';
const emit = defineEmits(['submit']); const emit = defineEmits(['submit']);
const name = ref(''); const name = ref('');
const species = ref<'cat' | 'dog'>('cat');
const isValid = computed(() => name.value.trim().length > 0 && name.value.length <= 12); const isValid = computed(() => name.value.trim().length > 0 && name.value.length <= 12);
const submit = () => { const submit = () => {
if (isValid.value) { if (isValid.value) {
emit('submit', name.value.trim()); emit('submit', { name: name.value.trim(), species: species.value });
} }
}; };
</script> </script>

View File

@ -1,31 +1,238 @@
<template> <template>
<div class="w-12 h-12 relative image-pixelated"> <div class="w-full h-full relative image-pixelated flex items-center justify-center" :class="{ 'grayscale': isDead }">
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" shapeRendering="crispEdges"> <svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" shapeRendering="crispEdges" class="w-full h-full">
<!-- Body/Head --> <!-- EGG STAGE -->
<rect x="6" y="2" width="4" height="4" :fill="finalSkin" /> <!-- Head --> <g v-if="isEgg">
<rect x="5" y="6" width="6" height="5" :fill="finalOutfit" /> <!-- Body --> <!-- Egg Base -->
<rect x="5" y="6" width="2" height="3" :fill="finalOutfit" filter="brightness(0.9)" /> <!-- Left Arm --> <path d="M12 4 H20 V6 H22 V10 H24 V22 H22 V26 H20 V28 H12 V26 H10 V22 H8 V10 H10 V6 H12 Z" :fill="eggBaseColor" />
<rect x="9" y="6" width="2" height="3" :fill="finalOutfit" filter="brightness(0.9)" /> <!-- Right Arm --> <!-- Egg Shading -->
<rect x="6" y="11" width="1" height="3" :fill="finalSkin" /> <!-- Leg L --> <path d="M12 26 H20 V28 H12 Z" fill="rgba(0,0,0,0.2)" />
<rect x="9" y="11" width="1" height="3" :fill="finalSkin" /> <!-- Leg R --> <path d="M22 10 H24 V22 H22 Z" fill="rgba(0,0,0,0.1)" />
<!-- Patterns -->
<!-- CAT Patterns -->
<g v-if="species === 'cat'">
<g v-if="eggPattern === 'spots'" fill="rgba(255,255,255,0.3)">
<rect x="14" y="8" width="2" height="2" />
<rect x="18" y="14" width="2" height="2" />
<rect x="12" y="18" width="2" height="2" />
<rect x="16" y="22" width="2" height="2" />
</g>
<g v-else-if="eggPattern === 'stripes'" fill="rgba(255,255,255,0.2)">
<rect x="10" y="10" width="12" height="2" />
<rect x="8" y="16" width="16" height="2" />
<rect x="10" y="22" width="12" height="2" />
</g>
<g v-else-if="eggPattern === 'zigzag'" fill="rgba(255,255,255,0.2)">
<path d="M10 14 H12 V12 H14 V14 H16 V12 H18 V14 H20 V12 H22 V14 H22 V16 H20 V18 H18 V16 H16 V18 H14 V16 H12 V18 H10 V16 Z" />
</g>
</g>
<!-- Hair --> <!-- DOG Patterns -->
<rect x="5" y="1" width="6" height="2" :fill="finalHair" /> <g v-else>
<rect x="4" y="2" width="1" height="3" :fill="finalHair" /> <!-- Bone Pattern -->
<rect x="11" y="2" width="1" height="3" :fill="finalHair" /> <g v-if="eggPattern === 'bone'" fill="rgba(255,255,255,0.3)">
<rect x="14" y="14" width="4" height="2" />
<rect x="13" y="13" width="1" height="1" />
<rect x="13" y="16" width="1" height="1" />
<rect x="18" y="13" width="1" height="1" />
<rect x="18" y="16" width="1" height="1" />
</g>
<!-- Paw Pattern -->
<g v-else-if="eggPattern === 'paw'" fill="rgba(255,255,255,0.2)">
<rect x="15" y="18" width="2" height="2" /> <!-- Pad -->
<rect x="14" y="16" width="1" height="1" />
<rect x="16" y="15" width="1" height="1" />
<rect x="18" y="16" width="1" height="1" />
</g>
<!-- Patch Pattern -->
<g v-else-if="eggPattern === 'patch'" fill="rgba(255,255,255,0.2)">
<rect x="18" y="8" width="4" height="4" />
<rect x="10" y="20" width="6" height="4" />
</g>
</g>
<!-- Cracks (if close to hatching) -->
<g v-if="isHatching" fill="#2b193f">
<rect x="14" y="6" width="1" height="4" />
<rect x="13" y="9" width="1" height="2" />
<rect x="15" y="8" width="1" height="3" />
</g>
</g>
<!-- Face --> <!-- DEITY STAGE -->
<rect x="7" y="4" width="1" height="1" fill="#000" opacity="0.6"/> <!-- Eye --> <g v-else-if="deityId">
<rect x="9" y="4" width="1" height="1" fill="#000" opacity="0.6"/> <!-- Eye --> <!-- Mazu (妈祖) -->
<g v-if="deityId === 'mazu'">
<rect x="10" y="4" width="12" height="12" fill="#ffe0bd" /> <!-- Face -->
<rect x="8" y="4" width="2" height="12" fill="#1a1a1a" /> <!-- Hair L -->
<rect x="22" y="4" width="2" height="12" fill="#1a1a1a" /> <!-- Hair R -->
<rect x="8" y="2" width="16" height="4" fill="#d4af37" /> <!-- Crown Base -->
<rect x="14" y="0" width="4" height="2" fill="#d4af37" /> <!-- Crown Top -->
<rect x="12" y="10" width="2" height="2" fill="#000" opacity="0.6" /> <!-- Eye -->
<rect x="18" y="10" width="2" height="2" fill="#000" opacity="0.6" /> <!-- Eye -->
<path d="M8 16 H24 V28 H8 Z" fill="#ffa500" /> <!-- Robe -->
<rect x="14" y="16" width="4" height="12" fill="#d4af37" opacity="0.5" /> <!-- Robe Detail -->
</g>
<!-- Accessory/Deity Specifics --> <!-- Earth God (土地公) -->
<rect v-if="deityId === 'Mazu'" x="5" y="0" width="6" height="1" fill="#d4af37" /> <!-- Crown --> <g v-else-if="deityId === 'earth_god'">
<rect v-if="deityId === 'EarthGod'" x="5" y="10" width="6" height="1" fill="#8e5c2e" /> <!-- Belt --> <rect x="10" y="6" width="12" height="10" fill="#f0c0a8" /> <!-- Face -->
<rect v-if="deityId === 'Matchmaker'" x="10" y="7" width="2" height="2" fill="#ff0000" /> <!-- Red Thread --> <rect x="8" y="4" width="16" height="4" fill="#1a1a1a" /> <!-- Hat Base -->
<rect x="10" y="2" width="12" height="2" fill="#1a1a1a" /> <!-- Hat Top -->
<rect x="12" y="10" width="2" height="2" fill="#000" opacity="0.6" /> <!-- Eye -->
<rect x="18" y="10" width="2" height="2" fill="#000" opacity="0.6" /> <!-- Eye -->
<rect x="10" y="14" width="12" height="6" fill="#f0f0f0" /> <!-- Beard -->
<path d="M8 16 H24 V28 H8 Z" fill="#d75b5b" /> <!-- Robe -->
<rect x="14" y="20" width="4" height="2" fill="#8e5c2e" /> <!-- Belt -->
</g>
<!-- Weapon --> <!-- Matchmaker (月老) -->
<path v-if="weapon === 'sword'" d="M11 9 L13 7 L14 8 L12 10 Z" fill="#ccc" /> <g v-else-if="deityId === 'matchmaker'">
<rect v-if="weapon === 'staff'" x="11" y="5" width="1" height="8" fill="#8d6e63" /> <rect x="10" y="6" width="12" height="10" fill="#ffe0bd" /> <!-- Face -->
<rect x="10" y="4" width="12" height="4" fill="#f0f0f0" /> <!-- Hair -->
<rect x="12" y="10" width="2" height="2" fill="#000" opacity="0.6" /> <!-- Eye -->
<rect x="18" y="10" width="2" height="2" fill="#000" opacity="0.6" /> <!-- Eye -->
<rect x="10" y="14" width="12" height="4" fill="#f0f0f0" /> <!-- Beard -->
<path d="M8 16 H24 V28 H8 Z" fill="#d95763" /> <!-- Robe -->
<rect x="20" y="18" width="4" height="4" fill="#ff0000" /> <!-- Red Thread Ball -->
</g>
<!-- Wenchang (文昌) -->
<g v-else>
<rect x="10" y="6" width="12" height="10" fill="#ffe0bd" /> <!-- Face -->
<rect x="8" y="2" width="16" height="6" fill="#1a1a1a" /> <!-- Hat -->
<rect x="12" y="10" width="2" height="2" fill="#000" opacity="0.6" /> <!-- Eye -->
<rect x="18" y="10" width="2" height="2" fill="#000" opacity="0.6" /> <!-- Eye -->
<rect x="12" y="14" width="8" height="2" fill="#1a1a1a" /> <!-- Mustache -->
<path d="M8 16 H24 V28 H8 Z" fill="#9fd75b" /> <!-- Robe -->
<rect x="20" y="18" width="2" height="8" fill="#8d6e63" /> <!-- Brush -->
</g>
</g>
<!-- PORTRAIT STAGE (Baby, Child, Adult) -->
<g v-else>
<!-- Background Glow (Optional) -->
<circle cx="16" cy="16" r="14" :fill="auraColor" opacity="0.2" />
<!-- CAT EARS -->
<g v-if="species === 'cat'" :fill="furColor">
<!-- Left Ear -->
<path d="M6 4 H10 V8 H6 Z" />
<path d="M7 5 H9 V7 H7 Z" fill="#ffb7b2" /> <!-- Inner Ear -->
<!-- Right Ear -->
<path d="M22 4 H26 V8 H22 Z" />
<path d="M23 5 H25 V7 H23 Z" fill="#ffb7b2" /> <!-- Inner Ear -->
</g>
<!-- DOG EARS (Floppy) -->
<g v-else :fill="furColor">
<!-- Left Ear -->
<path d="M4 8 H8 V14 H6 V16 H4 Z" />
<!-- Right Ear -->
<path d="M24 8 H28 V16 H26 V14 H24 Z" />
</g>
<!-- Head Base -->
<rect x="8" y="8" width="16" height="14" :fill="furColor" />
<rect x="6" y="10" width="2" height="10" :fill="furColor" /> <!-- Cheeks L -->
<rect x="24" y="10" width="2" height="10" :fill="furColor" /> <!-- Cheeks R -->
<!-- CAT STRIPES -->
<g v-if="species === 'cat'">
<!-- Tiger Stripes (Forehead) -->
<g fill="#4a3b5e" opacity="0.6">
<rect x="15" y="8" width="2" height="3" />
<rect x="12" y="9" width="1" height="2" />
<rect x="19" y="9" width="1" height="2" />
</g>
<!-- Cheeks Stripes -->
<g fill="#4a3b5e" opacity="0.4">
<rect x="6" y="14" width="2" height="1" />
<rect x="6" y="16" width="2" height="1" />
<rect x="24" y="14" width="2" height="1" />
<rect x="24" y="16" width="2" height="1" />
</g>
</g>
<!-- DOG PATCH (Eye Patch) -->
<g v-else>
<rect x="18" y="12" width="5" height="5" fill="#4a3b5e" opacity="0.3" />
</g>
<!-- Face Features -->
<!-- Eyes -->
<!-- Eyes -->
<g :fill="eyeColor">
<g v-if="mood === 'dead'">
<!-- X Eyes -->
<path d="M10 13 L13 16 M13 13 L10 16" stroke="#1b1026" stroke-width="1" />
<path d="M19 13 L22 16 M22 13 L19 16" stroke="#1b1026" stroke-width="1" />
</g>
<g v-else>
<rect x="10" y="13" width="3" height="3" />
<rect x="19" y="13" width="3" height="3" />
<!-- Highlights -->
<rect x="12" y="13" width="1" height="1" fill="white" />
<rect x="21" y="13" width="1" height="1" fill="white" />
</g>
</g>
<!-- Glasses (High INT) -->
<g v-if="stats.int > 20" stroke="#f6b26b" stroke-width="1" fill="none">
<rect x="9.5" y="12.5" width="4" height="4" />
<rect x="18.5" y="12.5" width="4" height="4" />
<line x1="13.5" y1="14.5" x2="18.5" y2="14.5" />
</g>
<!-- Nose & Mouth -->
<rect x="15" y="17" width="2" height="1" :fill="species === 'dog' ? '#1a1a1a' : '#ffb7b2'" /> <!-- Nose (Black for dog, Pink for cat) -->
<!-- Mouth Expressions -->
<g fill="#2b193f">
<g v-if="mood === 'happy'">
<rect x="14" y="19" width="1" height="1" />
<rect x="15" y="20" width="2" height="1" />
<rect x="17" y="19" width="1" height="1" />
</g>
<g v-else-if="mood === 'sad'">
<rect x="14" y="20" width="1" height="1" />
<rect x="15" y="19" width="2" height="1" />
<rect x="17" y="20" width="1" height="1" />
</g>
<g v-else-if="mood === 'dead'">
<!-- Dead Mouth (Flat line) -->
<rect x="14" y="20" width="4" height="1" />
</g>
<g v-else>
<rect x="14" y="19" width="4" height="1" />
</g>
</g>
<!-- Body (Shoulders/Upper Chest) - Portrait Style -->
<path d="M8 22 H24 V32 H8 Z" :fill="outfitColor" />
<path d="M6 24 H8 V32 H6 Z" :fill="outfitColor" filter="brightness(0.9)" />
<path d="M24 24 H26 V32 H24 Z" :fill="outfitColor" filter="brightness(0.9)" />
<!-- Collar/Accessory -->
<g v-if="stats.str > 20">
<!-- Red Scarf/Bandana -->
<path d="M10 22 H22 V24 H20 V25 H12 V24 H10 Z" fill="#d95763" />
</g>
<g v-else-if="stats.dex > 20">
<!-- Green Bowtie -->
<path d="M14 23 L12 22 V26 L14 25 Z" fill="#99e550" />
<path d="M18 23 L20 22 V26 L18 25 Z" fill="#99e550" />
<rect x="15" y="23" width="2" height="2" fill="#76c442" />
</g>
<g v-else>
<!-- Simple Collar -->
<rect x="11" y="22" width="10" height="2" fill="#e0d8f0" opacity="0.5" />
</g>
</g>
</svg> </svg>
</div> </div>
</template> </template>
@ -34,43 +241,96 @@
import { computed } from 'vue'; import { computed } from 'vue';
interface Props { interface Props {
skinColor?: string; stage?: string;
hairColor?: string; species?: string;
outfitColor?: string; stats?: {
weapon?: 'none' | 'sword' | 'staff'; str: number;
int: number;
dex: number;
happiness: number;
generation?: number;
age?: number;
};
skinColor?: string; // Fallback
outfitColor?: string; // Fallback
deityId?: string; deityId?: string;
isDead?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
stage: 'egg',
species: 'cat',
stats: () => ({ str: 0, int: 0, dex: 0, happiness: 50, generation: 1, age: 0 }),
skinColor: '#ffdbac', skinColor: '#ffdbac',
hairColor: '#5e412f',
outfitColor: '#78909c', outfitColor: '#78909c',
weapon: 'none' isDead: false
}); });
const finalSkin = computed(() => { const isEgg = computed(() => {
if (props.deityId === 'Mazu') return '#ffe0bd'; if (props.deityId) return false;
if (props.deityId === 'EarthGod') return '#f0c0a8'; return !props.stage || props.stage.toLowerCase() === 'egg';
if (props.deityId === 'Matchmaker') return '#ffe0bd';
if (props.deityId === 'Wenchang') return '#ffe0bd';
return props.skinColor;
}); });
const finalHair = computed(() => { const isHatching = computed(() => {
if (props.deityId === 'Mazu') return '#1a1a1a'; return isEgg.value && (props.stats.age || 0) > 60;
if (props.deityId === 'EarthGod') return '#f0f0f0';
if (props.deityId === 'Matchmaker') return '#f0f0f0';
if (props.deityId === 'Wenchang') return '#1a1a1a';
return props.hairColor;
}); });
const finalOutfit = computed(() => { // --- EGG LOGIC ---
if (props.deityId === 'Mazu') return '#ffa500';
if (props.deityId === 'EarthGod') return '#d75b5b'; const eggBaseColor = computed(() => {
if (props.deityId === 'Matchmaker') return '#d95763'; const { str, int, dex } = props.stats;
if (props.deityId === 'Wenchang') return '#9fd75b'; if (str > int && str > dex) return '#ffcccb'; // Reddish
return props.outfitColor; if (int > str && int > dex) return '#cce5ff'; // Bluish
if (dex > str && dex > int) return '#ccffcc'; // Greenish
return '#f0f0f0'; // White/Grey
}); });
const eggPattern = computed(() => {
const gen = props.stats.generation || 1;
if (props.species === 'cat') {
const patterns = ['spots', 'stripes', 'zigzag'];
return patterns[gen % patterns.length];
} else {
const patterns = ['bone', 'paw', 'patch'];
return patterns[gen % patterns.length];
}
});
// --- PORTRAIT LOGIC ---
const furColor = computed(() => {
const colors = ['#ffdbac', '#e0e0e0', '#d2b48c', '#ffdead'];
const gen = props.stats.generation || 1;
return colors[gen % colors.length];
});
const eyeColor = computed(() => {
const { str, int, dex } = props.stats;
if (int > 30) return '#2ce8f4'; // Cyan eyes for high INT
if (str > 30) return '#d95763'; // Red eyes for high STR
return '#1b1026'; // Black eyes default
});
const auraColor = computed(() => {
const { str, int, dex } = props.stats;
if (str > int && str > dex) return '#d95763';
if (int > str && int > dex) return '#2ce8f4';
if (dex > str && dex > int) return '#99e550';
return '#ffffff';
});
const mood = computed(() => {
if (props.isDead) return 'dead';
const h = props.stats.happiness;
if (h >= 70) return 'happy';
if (h <= 30) return 'sad';
return 'neutral';
});
const outfitColor = computed(() => {
return props.outfitColor;
});
</script> </script>
<style scoped> <style scoped>

View File

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

View File

@ -11,6 +11,96 @@
> >
<Trophy :size="14" class="text-[#f6b26b] group-hover:text-white" /> <Trophy :size="14" class="text-[#f6b26b] group-hover:text-white" />
</button> </button>
<!-- Evolution Help Button with Hover Tooltip -->
<div class="relative group/evo">
<button
class="p-1 bg-[#2b193f] border border-[#2ce8f4] hover:bg-[#3d2459] active:translate-y-0.5"
title="進化條件"
>
<HelpCircle :size="14" class="text-[#2ce8f4] group-hover/evo:text-white" />
</button>
<!-- Evolution Info Tooltip (Hover) -->
<div class="absolute right-0 top-full mt-1 w-48 px-3 py-2 bg-[#0f0816] border-2 border-[#2ce8f4] text-[10px] text-[#e0d8f0] font-mono opacity-0 invisible group-hover/evo:opacity-100 group-hover/evo:visible transition-all duration-200 z-50 shadow-[0_0_20px_rgba(44,232,244,0.3)]">
<div class="flex items-center justify-between mb-2 pb-1 border-b border-[#2ce8f4]">
<span class="text-[#2ce8f4] font-bold">進化條件</span>
</div>
<!-- Egg Baby -->
<div v-if="stats.class === 'egg'" class="space-y-1.5">
<p class="text-[#99e550] font-bold flex items-center gap-1">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M4 1H6V2H4V1ZM3 2H7V3H3V2ZM2 3H8V8H2V3ZM3 8H7V9H3V8Z" fill="#99e550"/><path d="M3 4H4V5H3V4ZM6 4H7V5H6V4ZM4 6H6V7H4V6Z" fill="#0f0816"/></svg>
幼年期
</p>
<p class="flex items-center gap-1 text-[#e0d8f0]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M2 1H8V2H2V1ZM1 2H9V8H1V2ZM2 8H8V9H2V8Z" fill="#f6b26b"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM3 5H7V6H3V5Z" fill="#0f0816"/></svg>
年齡達到: 5分鐘
</p>
<p class="text-[#8f80a0] text-[9px]">蛋孵化後自動進化</p>
</div>
<!-- Baby Child -->
<div v-else-if="stats.class === 'baby'" class="space-y-1.5">
<p class="text-[#99e550] font-bold flex items-center gap-1">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 1H7V2H3V1ZM2 2H3V3H2V2ZM7 2H8V3H7V2ZM1 3H2V6H1V3ZM8 3H9V6H8V3ZM2 6H3V7H2V6ZM7 6H8V7H7V6ZM3 7H7V9H3V7Z" fill="#99e550"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM4 5H6V6H4V5Z" fill="#0f0816"/></svg>
成長期
</p>
<p class="flex items-center gap-1 text-[#e0d8f0]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M2 1H8V2H2V1ZM1 2H9V8H1V2ZM2 8H8V9H2V8Z" fill="#f6b26b"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM3 5H7V6H3V5Z" fill="#0f0816"/></svg>
年齡達到: 6小時5分
</p>
<p class="flex items-center gap-1 text-[#f6b26b]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 2H7V3H3V2ZM2 3H3V6H2V3ZM7 3H8V6H7V3ZM3 6H4V7H3V6ZM6 6H7V7H6V6ZM4 6H6V8H4V6Z" fill="#f6b26b"/></svg>
力量 8
</p>
<p class="flex items-center gap-1 text-[#2ce8f4]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 1H7V2H3V1ZM2 2H8V8H2V2ZM3 8H7V9H3V8Z" fill="#2ce8f4"/><path d="M3 3H7V4H3V3ZM4 4H6V5H4V4ZM5 5H6V6H5V5Z" fill="#0f0816"/></svg>
智力 8
</p>
</div>
<!-- Child Adult -->
<div v-else-if="stats.class === 'child'" class="space-y-1.5">
<p class="text-[#99e550] font-bold flex items-center gap-1">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 1H7V2H3V1ZM2 2H8V3H2V2ZM1 3H2V7H1V3ZM8 3H9V7H8V3ZM2 7H3V8H2V7ZM7 7H8V8H7V7ZM3 8H7V9H3V8Z" fill="#99e550"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM3 5H7V6H3V5Z" fill="#0f0816"/></svg>
成熟期
</p>
<p class="flex items-center gap-1 text-[#e0d8f0]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M2 1H8V2H2V1ZM1 2H9V8H1V2ZM2 8H8V9H2V8Z" fill="#f6b26b"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM3 5H7V6H3V5Z" fill="#0f0816"/></svg>
年齡達到: 3天6小時5分
</p>
<p class="flex items-center gap-1 text-[#f6b26b]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 2H7V3H3V2ZM2 3H3V6H2V3ZM7 3H8V6H7V3ZM3 6H4V7H3V6ZM6 6H7V7H6V6ZM4 6H6V8H4V6Z" fill="#f6b26b"/></svg>
力量 50
</p>
<p class="flex items-center gap-1 text-[#2ce8f4]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 1H7V2H3V1ZM2 2H8V8H2V2ZM3 8H7V9H3V8Z" fill="#2ce8f4"/><path d="M3 3H7V4H3V3ZM4 4H6V5H4V4ZM5 5H6V6H5V5Z" fill="#0f0816"/></svg>
智力 50
</p>
<p class="flex items-center gap-1 text-[#99e550]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M4 1H6V2H4V1ZM3 2H7V3H3V2ZM2 3H8V6H2V3ZM3 6H7V8H3V6ZM4 8H6V9H4V8Z" fill="#99e550"/><path d="M4 3H5V4H4V3ZM5 4H6V5H5V4Z" fill="#0f0816"/></svg>
敏捷 50
</p>
</div>
<!-- Adult Evolution Branches -->
<div v-else-if="stats.class === 'adult'" class="space-y-1.5">
<p class="text-[#d95763] font-bold flex items-center gap-1">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M4 1H6V2H4V1ZM3 2H4V3H3V2ZM6 2H7V3H6V2ZM2 3H3V4H2V3ZM7 3H8V4H7V3ZM1 4H9V5H1V4ZM2 5H8V8H2V5ZM3 8H7V9H3V8Z" fill="#d95763"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM4 5H5V6H4V5Z" fill="#ffe762"/></svg>
已達最終階段
</p>
<p class="mt-1 text-[9px] text-[#8f80a0]">可能的進化分支:</p>
<p class="text-[9px] text-[#e0d8f0]">戰士猛虎 (STR優勢)</p>
<p class="text-[9px] text-[#e0d8f0]">敏捷靈貓 (DEX優勢)</p>
<p class="text-[9px] text-[#e0d8f0]">智者賢貓 (INT優勢)</p>
</div>
<!-- Tooltip Arrow -->
<div class="absolute top-0 right-2 w-0 h-0 border-l-4 border-l-transparent border-r-4 border-r-transparent border-b-4 border-b-[#2ce8f4] -translate-y-full"></div>
</div>
</div>
<button <button
@click="$emit('deletePet')" @click="$emit('deletePet')"
class="p-1 bg-[#2b193f] border border-[#d95763] hover:bg-[#3d2459] active:translate-y-0.5 group" class="p-1 bg-[#2b193f] border border-[#d95763] hover:bg-[#3d2459] active:translate-y-0.5 group"
@ -26,11 +116,21 @@
<div class="absolute inset-0 bg-[#2b193f] opacity-50 overflow-hidden" /> <div class="absolute inset-0 bg-[#2b193f] opacity-50 overflow-hidden" />
<!-- The Animated Pixel Avatar --> <!-- The Animated Pixel Avatar -->
<div class="scale-110 transform translate-y-1 relative z-10"> <div class="w-full h-full relative z-10">
<PixelAvatar <PixelAvatar
:stage="stats.class || stats.stage"
:species="stats.species"
:stats="{
str: stats.str,
int: stats.int,
dex: stats.dex,
happiness: stats.happiness,
generation: stats.generation,
age: stats.ageSeconds
}"
skinColor="#ffdbac" skinColor="#ffdbac"
hairColor="#e0d8f0"
outfitColor="#9fd75b" outfitColor="#9fd75b"
:is-dead="stats.isDead"
/> />
</div> </div>
@ -63,11 +163,19 @@
<path d="M3 7H7V8H3V7Z" fill="#1b1026"/> <path d="M3 7H7V8H3V7Z" fill="#1b1026"/>
</g> </g>
<!-- Sad --> <!-- Sad -->
<g v-else> <g v-else-if="mood.type === 'sad'">
<path d="M2 4H3V6H2V4ZM7 4H8V6H7V4Z" fill="#1b1026"/> <path d="M2 4H3V6H2V4ZM7 4H8V6H7V4Z" fill="#1b1026"/>
<path d="M3 7H4V8H3V7ZM6 7H7V8H6V7ZM4 6H6V7H4V6Z" fill="#1b1026"/> <path d="M3 7H4V8H3V7ZM6 7H7V8H6V7ZM4 6H6V7H4V6Z" fill="#1b1026"/>
<path d="M2 6H3V7H2V6ZM7 6H8V7H7V6Z" fill="#2ce8f4" fill-opacity="0.5"/> <path d="M2 6H3V7H2V6ZM7 6H8V7H7V6Z" fill="#2ce8f4" fill-opacity="0.5"/>
</g> </g>
<!-- Dead -->
<g v-else-if="mood.type === 'dead'">
<path d="M2 3H3V4H4V5H3V4H2V3ZM4 3H5V4H4V3Z" fill="#1b1026"/>
<path d="M7 3H8V4H9V5H8V4H7V3ZM9 3H10V4H9V3Z" fill="#1b1026"/>
<path d="M2 5H3V6H2V5ZM4 5H5V6H4V5Z" fill="#1b1026"/>
<path d="M7 5H8V6H7V5ZM9 5H10V6H9V5Z" fill="#1b1026"/>
<path d="M3 7H8V8H3V7Z" fill="#1b1026"/>
</g>
</svg> </svg>
<span class="text-[10px] font-bold font-mono leading-none">{{ mood.text }}</span> <span class="text-[10px] font-bold font-mono leading-none">{{ mood.text }}</span>
</div> </div>
@ -120,7 +228,7 @@
<!-- Vitals - Updated to Health, Hunger, Happiness --> <!-- Vitals - Updated to Health, Hunger, Happiness -->
<div class="flex flex-col gap-2 px-1 mb-2"> <div class="flex flex-col gap-2 px-1 mb-2">
<!-- Health (Heart) --> <!-- Health (Heart) -->
<RetroResourceBar :current="stats.hp" :max="stats.maxHp" type="hp" label="生命" :segments="10"> <RetroResourceBar :current="stats.hp" :max="stats.maxHp" type="hp" label="健康" :segments="10">
<template #icon> <template #icon>
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 2H4V3H2V2ZM6 2H8V3H6V2ZM1 3H2V5H1V3ZM8 3H9V5H8V3ZM2 5H3V6H2V5ZM7 5H8V6H7V5ZM3 6H4V7H3V6ZM6 6H7V7H6V6ZM4 7H6V8H4V7Z" fill="#d95763"/> <path d="M2 2H4V3H2V2ZM6 2H8V3H6V2ZM1 3H2V5H1V3ZM8 3H9V5H8V3ZM2 5H3V6H2V5ZM7 5H8V6H7V5ZM3 6H4V7H3V6ZM6 6H7V7H6V6ZM4 7H6V8H4V7Z" fill="#d95763"/>
@ -183,6 +291,14 @@
<!-- Core Stats --> <!-- Core Stats -->
<div class="grid grid-cols-2 gap-x-4 gap-y-2 pt-2 border-t border-[#4a3b5e]"> <div class="grid grid-cols-2 gap-x-4 gap-y-2 pt-2 border-t border-[#4a3b5e]">
<div class="flex justify-between items-center px-1">
<span class="text-[10px] text-[#d95763] font-mono">生命</span>
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.maxHp || 100) }}</span>
</div>
<div class="flex justify-between items-center px-1">
<span class="text-[10px] text-[#d95763] font-mono">運氣</span>
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.luck || 0) }}</span>
</div>
<div class="flex justify-between items-center px-1"> <div class="flex justify-between items-center px-1">
<span class="text-[10px] text-[#f6b26b] font-mono">力量</span> <span class="text-[10px] text-[#f6b26b] font-mono">力量</span>
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.str || 0) }}</span> <span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.str || 0) }}</span>
@ -195,10 +311,6 @@
<span class="text-[10px] text-[#99e550] font-mono">敏捷</span> <span class="text-[10px] text-[#99e550] font-mono">敏捷</span>
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.dex || 0) }}</span> <span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.dex || 0) }}</span>
</div> </div>
<div class="flex justify-between items-center px-1">
<span class="text-[10px] text-[#d95763] font-mono">運氣</span>
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.luck || 0) }}</span>
</div>
</div> </div>
<!-- Combat Stats --> <!-- Combat Stats -->
@ -336,8 +448,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed, ref } from 'vue';
import { Trophy, Trash2, Sparkles, Star, Gem, Circle, Leaf } from 'lucide-vue-next'; import { Trophy, Trash2, Sparkles, Star, Gem, Circle, Leaf, HelpCircle } from 'lucide-vue-next';
import PixelFrame from './PixelFrame.vue'; import PixelFrame from './PixelFrame.vue';
import RetroResourceBar from './RetroResourceBar.vue'; import RetroResourceBar from './RetroResourceBar.vue';
import RetroCounter from './RetroCounter.vue'; import RetroCounter from './RetroCounter.vue';
@ -355,6 +467,7 @@ defineEmits(['openAchievements', 'deletePet', 'openInventory']);
// Determine mood based on happiness // Determine mood based on happiness
const mood = computed(() => { const mood = computed(() => {
if (props.stats.isDead) return { icon: '💀', text: '死亡', type: 'dead' };
const happiness = props.stats.happiness || 0; const happiness = props.stats.happiness || 0;
if (happiness >= 80) return { icon: '😄', text: '開心', type: 'happy' }; if (happiness >= 80) return { icon: '😄', text: '開心', type: 'happy' };
if (happiness >= 50) return { icon: '🙂', text: '平靜', type: 'calm' }; if (happiness >= 50) return { icon: '🙂', text: '平靜', type: 'calm' };

View File

@ -46,6 +46,7 @@
@openShop="showShop = true" @openShop="showShop = true"
@openAdventure="showAdventureSelect = true" @openAdventure="showAdventureSelect = true"
@toggleSleep="handleToggleSleep" @toggleSleep="handleToggleSleep"
@debugAddItems="handleDebugAddItems"
/> />
</div> </div>
</div> </div>
@ -68,7 +69,7 @@
@close="showAchievements = false" @close="showAchievements = false"
title="成就" title="成就"
> >
<AchievementsOverlay :achievements="ACHIEVEMENTS_DATA" /> <AchievementsOverlay :achievements="achievementsState" />
</PixelModal> </PixelModal>
<!-- Inventory Overlay --> <!-- Inventory Overlay -->
@ -83,6 +84,7 @@
@unequip="handleUnequip" @unequip="handleUnequip"
@use="handleUseItem" @use="handleUseItem"
@delete="handleDeleteItem" @delete="handleDeleteItem"
@repair="handleRepair"
/> />
</PixelModal> </PixelModal>
@ -172,6 +174,7 @@ import AdventureOverlay from '~/components/pixel/AdventureOverlay.vue';
import { PetSystem } from '../../core/pet-system.js'; import { PetSystem } from '../../core/pet-system.js';
import { TempleSystem } from '../../core/temple-system.js'; import { TempleSystem } from '../../core/temple-system.js';
import { AchievementSystem } from '../../core/achievement-system.js';
import { ApiService } from '../../core/api-service.js'; import { ApiService } from '../../core/api-service.js';
// Import from data and core instead of types // Import from data and core instead of types
@ -192,6 +195,7 @@ type AdventureLocation = typeof ADVENTURES[0];
const apiService = new ApiService({ useMock: true }); // Use localStorage mock for now const apiService = new ApiService({ useMock: true }); // Use localStorage mock for now
const petSystem = ref<PetSystem | null>(null); const petSystem = ref<PetSystem | null>(null);
const templeSystem = ref<TempleSystem | null>(null); const templeSystem = ref<TempleSystem | null>(null);
const achievementSystem = ref<AchievementSystem | null>(null);
const initialized = ref(false); const initialized = ref(false);
// --- RESPONSIVE --- // --- RESPONSIVE ---
@ -214,6 +218,7 @@ if (typeof window !== 'undefined') {
// Reactive state mapped from PetSystem // Reactive state mapped from PetSystem
const systemState = ref<any>(null); const systemState = ref<any>(null);
const allDeities = ref<Deity[]>([]); const allDeities = ref<Deity[]>([]);
const achievementsState = ref<any[]>([]);
const playerStats = computed<EntityStats>(() => { const playerStats = computed<EntityStats>(() => {
if (!systemState.value) return { if (!systemState.value) return {
@ -233,6 +238,7 @@ const playerStats = computed<EntityStats>(() => {
return { return {
name: s.name || "Pet", name: s.name || "Pet",
species: s.species || 'cat',
class: s.stage, class: s.stage,
hp: Math.floor(s.health), hp: Math.floor(s.health),
maxHp: 100, maxHp: 100,
@ -246,6 +252,7 @@ const playerStats = computed<EntityStats>(() => {
maxHappiness: 100, maxHappiness: 100,
age: formatAge(currentAge), age: formatAge(currentAge),
ageSeconds: Math.floor(currentAge),
generation: s.generation || 1, generation: s.generation || 1,
height: `${s.height || 0} cm`, height: `${s.height || 0} cm`,
weight: `${Math.floor(s.weight || 0)} g`, weight: `${Math.floor(s.weight || 0)} g`,
@ -307,9 +314,17 @@ const isFighting = ref(false);
const battleLogs = ref<string[]>([]); const battleLogs = ref<string[]>([]);
const showNamingOverlay = ref(false); const showNamingOverlay = ref(false);
const handleNameSubmit = async (name: string) => { const handleNameSubmit = async (payload: { name: string, species: string }) => {
if (petSystem.value) { if (petSystem.value) {
await petSystem.value.updateState({ name }); const speciesId = payload.species === 'dog' ? 'tinyPuppy' : 'tinyTigerCat';
await petSystem.value.updateState({
name: payload.name,
speciesId: speciesId,
species: payload.species // Keep this for UI if needed, or rely on speciesId
});
// Force reload of species config in system if needed, or re-initialize
// Since updateState might not reload config, we might need a specific method or just handle it in updateState
systemState.value = petSystem.value.getState(); systemState.value = petSystem.value.getState();
showNamingOverlay.value = false; showNamingOverlay.value = false;
} }
@ -319,6 +334,9 @@ const handleDeletePet = async () => {
if (confirm('確定要刪除寵物嗎?此操作無法撤銷!(Are you sure you want to delete your pet?)')) { if (confirm('確定要刪除寵物嗎?此操作無法撤銷!(Are you sure you want to delete your pet?)')) {
if (petSystem.value) { if (petSystem.value) {
await petSystem.value.deletePet(); await petSystem.value.deletePet();
if (achievementSystem.value) {
await achievementSystem.value.reset();
}
location.reload(); // Reload to reset state and trigger new game flow location.reload(); // Reload to reset state and trigger new game flow
} }
} }
@ -332,6 +350,10 @@ onMounted(async () => {
// Initialize Systems // Initialize Systems
petSystem.value = new PetSystem(apiService); petSystem.value = new PetSystem(apiService);
templeSystem.value = new TempleSystem(apiService, petSystem.value); templeSystem.value = new TempleSystem(apiService, petSystem.value);
achievementSystem.value = new AchievementSystem(petSystem.value, null, templeSystem.value, apiService);
// Connect achievementSystem to petSystem (circular reference after creation)
petSystem.value.achievementSystem = achievementSystem.value;
// Load Data // Load Data
console.log("Initializing PetSystem..."); console.log("Initializing PetSystem...");
@ -339,19 +361,24 @@ onMounted(async () => {
console.log("Initial State:", state); console.log("Initial State:", state);
systemState.value = state; systemState.value = state;
// Initialize other systems
await templeSystem.value.initialize();
await achievementSystem.value.initialize();
// Check if naming is required // Check if naming is required
if (!state.name) { if (!state.name) {
showNamingOverlay.value = true; showNamingOverlay.value = true;
} }
// Load Deities // Load Deities
allDeities.value = await templeSystem.value.getDeities(); allDeities.value = templeSystem.value.getDeities();
// Start Game Loop with callback to update UI // Start Game Loop with callback to update UI
petSystem.value.startTickLoop((newState: any) => { petSystem.value.startTickLoop((newState: any) => {
// Use nextTick to ensure safe state updates // Use nextTick to ensure safe state updates
if (newState && petSystem.value) { if (newState && petSystem.value) {
systemState.value = { ...newState }; systemState.value = { ...newState };
updateAchievementsState();
} }
}); });
@ -362,6 +389,7 @@ onMounted(async () => {
const currentState = petSystem.value.getState(); const currentState = petSystem.value.getState();
if (currentState) { if (currentState) {
systemState.value = { ...currentState }; systemState.value = { ...currentState };
updateAchievementsState();
} }
} catch (error) { } catch (error) {
console.error('Error updating state:', error); console.error('Error updating state:', error);
@ -515,20 +543,177 @@ const handleToggleSleep = async () => {
} }
}; };
const handleEquip = async (itemId: string, asAppearance: boolean) => {
console.log("Equip not fully implemented in core yet", itemId); // ... (imports)
// Helper to recalculate stats based on equipped items
const recalculateStats = (inventory: any[]) => {
const newBuffs: { flat: Record<string, number>, percent: Record<string, number> } = { flat: {}, percent: {} };
inventory.forEach(item => {
if (item.isEquipped && !item.isAppearance && item.effects) {
// Apply flat effects
if (item.effects.flat) {
Object.entries(item.effects.flat).forEach(([key, value]) => {
newBuffs.flat[key] = (newBuffs.flat[key] || 0) + (value as number);
});
}
// Apply percent effects
if (item.effects.percent) {
Object.entries(item.effects.percent).forEach(([key, value]) => {
newBuffs.percent[key] = (newBuffs.percent[key] || 0) + (value as number);
});
}
}
});
return newBuffs;
}; };
const handleUnequip = async (slot: EquipSlot, asAppearance: boolean) => { const handleEquip = async (itemId: string, asAppearance: boolean) => {
console.log("Unequip not fully implemented in core yet", slot); if (!petSystem.value) return;
const currentState = petSystem.value.getState();
const inventory = [...currentState.inventory];
const itemIndex = inventory.findIndex((i: any) => i.id === itemId);
if (itemIndex === -1) return;
const item = inventory[itemIndex];
const slot = item.slot;
if (!slot) return; // Should not happen for equipment
// Unequip existing item in the same slot and mode
const existingItemIndex = inventory.findIndex((i: any) =>
i.isEquipped && i.slot === slot && !!i.isAppearance === asAppearance
);
if (existingItemIndex !== -1) {
inventory[existingItemIndex].isEquipped = false;
inventory[existingItemIndex].isAppearance = false;
}
// Equip new item and mark as having been equipped (for durability tracking)
inventory[itemIndex].isEquipped = true;
inventory[itemIndex].isAppearance = asAppearance;
inventory[itemIndex].hasBeenEquipped = true; // Mark that this item has been used
// Recalculate stats
const newEquipmentBuffs = recalculateStats(inventory);
await petSystem.value.updateState({
inventory,
equipmentBuffs: newEquipmentBuffs
});
systemState.value = petSystem.value.getState();
};
const handleUnequip = async (slot: string, asAppearance: boolean) => {
if (!petSystem.value) return;
const currentState = petSystem.value.getState();
const inventory = [...currentState.inventory];
const itemIndex = inventory.findIndex((i: any) =>
i.isEquipped && i.slot === slot && !!i.isAppearance === asAppearance
);
if (itemIndex !== -1) {
inventory[itemIndex].isEquipped = false;
inventory[itemIndex].isAppearance = false;
// Recalculate stats
const newEquipmentBuffs = recalculateStats(inventory);
await petSystem.value.updateState({
inventory,
equipmentBuffs: newEquipmentBuffs
});
systemState.value = petSystem.value.getState();
}
}; };
const handleUseItem = async (itemId: string) => { const handleUseItem = async (itemId: string) => {
console.log("Use item not fully implemented in core yet", itemId); if (!petSystem.value) return;
const currentState = petSystem.value.getState();
let inventory = [...currentState.inventory];
const itemIndex = inventory.findIndex((i: any) => i.id === itemId);
if (itemIndex === -1) return;
const item = inventory[itemIndex];
// Apply effects
if (item.effects) {
const updates: any = {};
// Handle modifyStats (e.g. hunger, happiness, health)
if (item.effects.modifyStats) {
Object.entries(item.effects.modifyStats).forEach(([key, value]) => {
const currentVal = currentState[key] || 0;
// Simple clamp to 0-100 for basic stats, though petSystem might handle it
// For now, let's just add it and let petSystem clamp if needed, or clamp here
let newVal = currentVal + (value as number);
if (['hunger', 'happiness', 'health'].includes(key)) {
newVal = Math.min(100, Math.max(0, newVal));
}
updates[key] = newVal;
});
}
// Handle cureSickness
if (item.effects.cureSickness) {
updates.isSick = false;
}
// Handle addBuff (simplified)
if (item.effects.addBuff) {
// This would require a more complex buff system in petSystem
console.log("Buffs not fully implemented yet", item.effects.addBuff);
}
// Apply updates
if (Object.keys(updates).length > 0) {
await petSystem.value.updateState(updates);
}
}
// Reduce quantity or remove
if (item.stackable && item.quantity > 1) {
inventory[itemIndex].quantity--;
} else {
inventory.splice(itemIndex, 1);
}
await petSystem.value.updateState({ inventory });
systemState.value = petSystem.value.getState();
}; };
const handleDeleteItem = async (itemId: string) => { const handleDeleteItem = async (itemId: string) => {
console.log("Delete item not fully implemented in core yet", itemId); if (!petSystem.value) return;
if (!confirm('確定要丟棄這個物品嗎?')) return;
const currentState = petSystem.value.getState();
const inventory = [...currentState.inventory];
const itemIndex = inventory.findIndex((i: any) => i.id === itemId);
if (itemIndex !== -1) {
const item = inventory[itemIndex];
inventory.splice(itemIndex, 1);
// Recalculate stats in case an equipped item was deleted
const newEquipmentBuffs = recalculateStats(inventory);
await petSystem.value.updateState({
inventory,
equipmentBuffs: newEquipmentBuffs
});
systemState.value = petSystem.value.getState();
}
}; };
const handleSwitchDeity = async (id: string) => { const handleSwitchDeity = async (id: string) => {
@ -546,8 +731,9 @@ const handleAddFavor = async (amount: number) => {
}; };
const handleBuyItem = async (item: Item) => { const handleBuyItem = async (item: Item) => {
if (petSystem.value && systemState.value.coins >= item.price) { const price = (item as any).price || 100;
const newCoins = systemState.value.coins - item.price; if (petSystem.value && systemState.value.coins >= price) {
const newCoins = systemState.value.coins - price;
const newInventory = [...(systemState.value.inventory || []), { ...item, id: `buy-${Date.now()}` }]; const newInventory = [...(systemState.value.inventory || []), { ...item, id: `buy-${Date.now()}` }];
await petSystem.value.updateState({ coins: newCoins, inventory: newInventory }); await petSystem.value.updateState({ coins: newCoins, inventory: newInventory });
systemState.value = petSystem.value.getState(); systemState.value = petSystem.value.getState();
@ -556,6 +742,57 @@ const handleBuyItem = async (item: Item) => {
} }
}; };
const handleDebugAddItems = async () => {
if (!petSystem.value) return;
// Add ALL items from ITEMS definition
const newItems = Object.values(ITEMS).map((itemDef: any) => {
const newItem = { ...itemDef };
newItem.id = `${newItem.id}_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
// For debug purposes, set items at full durability (not randomized)
// Durability will only decrease once the item has been equipped (hasBeenEquipped = true)
newItem.quantity = 1;
newItem.isEquipped = false;
// Don't set hasBeenEquipped - it should be undefined for new items
return newItem;
});
const currentInventory = systemState.value.inventory || [];
const updatedInventory = [...currentInventory, ...newItems];
await petSystem.value.updateState({ inventory: updatedInventory });
systemState.value = petSystem.value.getState();
};
const handleRepair = async (itemId: string) => {
if (!petSystem.value) return;
const currentState = petSystem.value.getState();
const inventory = [...currentState.inventory];
const itemIndex = inventory.findIndex((i: any) => i.id === itemId);
if (itemIndex === -1) return;
const item = inventory[itemIndex];
if (!item.maxDurability || item.maxDurability === Infinity) return;
const missingDurability = item.maxDurability - item.durability;
if (missingDurability <= 0) return;
const cost = Math.ceil(missingDurability * 0.5);
if (currentState.coins >= cost) {
inventory[itemIndex].durability = item.maxDurability;
await petSystem.value.updateState({
coins: currentState.coins - cost,
inventory
});
systemState.value = petSystem.value.getState();
console.log(`🔨 Repaired item ${item.name} for ${cost} gold`);
}
};
const handleSellItem = async (item: Item) => { const handleSellItem = async (item: Item) => {
if (petSystem.value) { if (petSystem.value) {
const sellPrice = Math.floor((item as any).price / 2); const sellPrice = Math.floor((item as any).price / 2);
@ -569,6 +806,22 @@ const handleSellItem = async (item: Item) => {
// Use imported data instead of mock // Use imported data instead of mock
const ADVENTURE_LOCATIONS = ADVENTURES; const ADVENTURE_LOCATIONS = ADVENTURES;
const ACHIEVEMENTS_DATA = ACHIEVEMENTS; const ACHIEVEMENTS_DATA = ACHIEVEMENTS;
const updateAchievementsState = () => {
if (achievementSystem.value) {
const all = achievementSystem.value.achievements;
achievementsState.value = all.map(achievement => {
const progress = achievementSystem.value!.getAchievementProgress(achievement);
const isUnlocked = achievementSystem.value!.unlockedAchievements.includes(achievement.id);
return {
...achievement,
unlocked: isUnlocked,
currentValue: progress.current,
maxValue: progress.target,
progress: Math.floor(progress.progress || 0)
};
});
}
};
// Convert ITEMS object to array for shop // Convert ITEMS object to array for shop
const SHOP_ITEMS = Object.values(ITEMS).filter((item: any) => item.type === 'consumable' || item.type === 'equipment').map((item: any) => ({ const SHOP_ITEMS = Object.values(ITEMS).filter((item: any) => item.type === 'consumable' || item.type === 'equipment').map((item: any) => ({

49
app/utils/formatters.js Normal file
View File

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

View File

@ -340,6 +340,13 @@ export class PetSystem {
// 更新狀態(同步到 API // 更新狀態(同步到 API
async updateState(updates) { async updateState(updates) {
// 如果更新了 speciesId重新載入配置
if (updates.speciesId && updates.speciesId !== this.state.speciesId) {
this.speciesConfig = PET_SPECIES[updates.speciesId]
// 可能需要重新計算屬性或重置某些狀態?
// 暫時只更新配置
}
this.state = { ...this.state, ...updates } this.state = { ...this.state, ...updates }
try { try {
@ -541,6 +548,32 @@ export class PetSystem {
} }
// 減少裝備耐久度(每 tick 減少少量,戰鬥或特殊情況會減少更多) // 減少裝備耐久度(每 tick 減少少量,戰鬥或特殊情況會減少更多)
// 只對已裝備過的物品減少耐久度hasBeenEquipped = true
if (this.state.inventory && Array.isArray(this.state.inventory)) {
const equippedItems = this.state.inventory.filter(item => item.isEquipped && !item.isAppearance);
for (const item of equippedItems) {
// 只對已經被裝備過的物品減少耐久度
if (item.hasBeenEquipped && item.maxDurability && item.maxDurability !== Infinity && item.durability > 0) {
// 每 10 個 tick 減少 1 點耐久度(約每 30 秒)
if (this.state._tickCount && this.state._tickCount % 10 === 0) {
item.durability = Math.max(0, item.durability - 1);
// 如果耐久度降為 0自動卸下裝備
if (item.durability <= 0) {
item.isEquipped = false;
item.isAppearance = false;
console.log(`⚠️ ${item.name} 耐久度耗盡,已自動卸下`);
// 重新計算裝備加成
this.calculateCombatStats();
}
}
}
}
}
// Legacy inventory system support (如果使用舊的 inventorySystem)
if (this.inventorySystem) { if (this.inventorySystem) {
// 只有裝備中的道具才會減少耐久度 // 只有裝備中的道具才會減少耐久度
const equipped = this.inventorySystem.getEquipped() const equipped = this.inventorySystem.getEquipped()

View File

@ -217,6 +217,194 @@ export const PET_SPECIES = {
} }
], ],
personality: ['活潑', '黏人'] personality: ['活潑', '黏人']
},
tinyPuppy: {
id: 'tinyPuppy',
name: '小幼犬',
description: '忠誠活潑的小狗狗',
baseStats: {
physiologyTickInterval: 1000,
eventCheckInterval: 10000,
hungerDecayPerTick: 0.025, // 狗狗比較容易餓 (原 0.02)
happinessDecayPerTick: 0.02, // 狗狗比較容易開心 (原 0.033)
poopChancePerTick: 0.06, // 狗狗比較容易便便
poopHealthDamage: 0.5,
hungerHealthDamage: 1,
sicknessThreshold: 40,
dyingTimeSeconds: 7200,
sleepSchedule: {
nightSleep: {
startHour: 22, // 晚上 22:00 開始 (比貓晚睡)
startMinute: 0,
endHour: 7, // 早上 7:00 結束 (比貓早起)
endMinute: 0,
autoSleep: true,
randomWakeChance: 0.4
},
noonNap: {
startHour: 13,
startMinute: 0,
endHour: 14,
endMinute: 0,
autoSleep: true,
randomWakeChance: 0.2
}
},
maxPoopCount: 5,
sleepDecayMultiplier: 0.1,
defaultHeight: 12, // 狗狗比較大隻
defaultWeight: 600,
playHungerCost: 2, // 玩耍消耗更多體力
combatFormulas: {
attack: {
strMultiplier: 3.0, // 攻擊力更高
dexMultiplier: 0.2
},
defense: {
strMultiplier: 1.5,
intMultiplier: 1.5
},
speed: {
dexMultiplier: 2.0, // 速度稍慢
intMultiplier: 0.5
}
},
dropRate: 0.1,
luck: 15 // 狗狗運氣比較好?
},
lifecycle: [
{
stage: 'egg',
durationSeconds: 300,
height: 6,
baseWeight: 120,
weightRange: { min: 100, max: 140 },
unlockedFeatures: [],
allowedActions: [],
enabledSystems: {
hunger: false,
happiness: false,
poop: false,
sickness: false,
sleep: false
}
},
{
stage: 'baby',
durationSeconds: 21900,
height: 12,
baseWeight: 250,
weightRange: { min: 220, max: 280 },
unlockedFeatures: [],
allowedActions: ['feed', 'play', 'clean', 'heal', 'sleep'],
enabledSystems: {
hunger: true,
happiness: true,
poop: true,
sickness: true,
sleep: true
}
},
{
stage: 'child',
durationSeconds: 281100,
height: 20,
baseWeight: 500,
weightRange: { min: 450, max: 550 },
unlockedFeatures: ['miniGames'],
allowedActions: ['feed', 'play', 'clean', 'heal', 'sleep'],
enabledSystems: {
hunger: true,
happiness: true,
poop: true,
sickness: true,
sleep: true
},
conditions: {
str: 10,
int: 6
}
},
{
stage: 'adult',
durationSeconds: Infinity,
height: 30,
baseWeight: 800,
weightRange: { min: 700, max: 900 },
unlockedFeatures: ['miniGames', 'breeding'],
allowedActions: ['feed', 'play', 'clean', 'heal', 'sleep'],
enabledSystems: {
hunger: true,
happiness: true,
poop: true,
sickness: true,
sleep: true
},
conditions: {
str: 60,
int: 40,
dex: 40
},
evolutions: [
{
id: 'guardian_dog',
name: '守護神犬',
icon: '🐕',
description: '忠誠的守護者',
conditions: {
str: { min: 60, dominant: true }
},
statModifiers: {
attack: 1.4,
defense: 1.4,
speed: 0.9
}
},
{
id: 'hunting_dog',
name: '獵犬',
icon: '🐩',
description: '敏銳的獵手',
conditions: {
dex: { min: 50, dominant: true }
},
statModifiers: {
attack: 1.2,
defense: 0.9,
speed: 1.3
}
},
{
id: 'guide_dog',
name: '導盲犬',
icon: '🦮',
description: '聰明的嚮導',
conditions: {
int: { min: 50, dominant: true }
},
statModifiers: {
attack: 0.8,
defense: 1.2,
speed: 1.0,
magic: 1.4
}
},
{
id: 'balanced_dog',
name: '成年犬',
icon: '🐕‍🦺',
description: '健康的成年犬',
conditions: {},
statModifiers: {
attack: 1.1,
defense: 1.1,
speed: 1.0
}
}
]
}
],
personality: ['忠誠', '憨厚']
} }
} }