good version
This commit is contained in:
parent
8def70de92
commit
086a7b796a
|
|
@ -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 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>
|
</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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center">
|
||||||
|
<span class="text-[9px] text-[#8f80a0] mb-1">装备</span>
|
||||||
|
<div v-if="getEquippedItem(key, false)" class="flex flex-col items-center gap-1">
|
||||||
|
<div class="relative w-8 h-8">
|
||||||
|
<PixelItemIcon :category="getEquippedItem(key, false)!.category" :color="ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color" />
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-center truncate w-full px-1 font-bold" :style="{ color: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color }">{{ getEquippedItem(key, false)!.name }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-[10px] text-[#4a3b5e] z-10 opacity-50">空</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 外观槽 -->
|
||||||
|
<div class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center">
|
||||||
|
<span class="text-[9px] text-[#8f80a0] mb-1">外觀</span>
|
||||||
|
<div v-if="getEquippedItem(key, true)" class="flex flex-col items-center gap-1">
|
||||||
|
<div class="relative w-8 h-8">
|
||||||
|
<PixelItemIcon :category="getEquippedItem(key, true)!.category" :color="ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color" />
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-center truncate w-full px-1 font-bold" :style="{ color: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color }">{{ getEquippedItem(key, true)!.name }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-[10px] text-[#4a3b5e] z-10 opacity-50">空</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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})`">
|
||||||
|
<div class="overflow-y-auto p-3 custom-scrollbar" style="max-height: calc(100vh - 400px);">
|
||||||
|
<div class="grid grid-cols-6 gap-2">
|
||||||
|
<!-- 30个格子(6x5) -->
|
||||||
|
<div v-for="index in 30" :key="index" class="relative aspect-square">
|
||||||
|
|
||||||
|
<!-- 有物品:显示物品 + 悬浮卡片 -->
|
||||||
|
<template v-if="getInventorySlotItem(index - 1)">
|
||||||
|
<button class="w-full h-full p-2 flex flex-col items-center justify-center gap-1 border-2 transition-all group rounded-sm border-[#4a3b5e] hover:border-[#8f80a0] hover:bg-[#321e4a] bg-[#2b193f]">
|
||||||
|
<div class="relative w-10 h-10">
|
||||||
|
<PixelItemIcon
|
||||||
|
:category="getInventorySlotItem(index - 1).category"
|
||||||
|
:color="ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 稀有度发光 -->
|
||||||
<div
|
<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"
|
v-if="['rare', 'epic', 'legendary'].includes(getInventorySlotItem(index - 1).rarity)"
|
||||||
@click="getEquippedItem(slot, false) && setSelectedItemId(getEquippedItem(slot, false)?.id)"
|
class="absolute inset-0 rounded-full opacity-40 blur-md pointer-events-none"
|
||||||
>
|
:style="{ backgroundColor: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color }"
|
||||||
<span class="text-[9px] text-[#8f80a0] mb-0.5">ACTUAL</span>
|
></div>
|
||||||
<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>
|
<span
|
||||||
|
v-if="getInventorySlotItem(index - 1).quantity && getInventorySlotItem(index - 1).quantity > 1"
|
||||||
|
class="absolute -bottom-1 -right-1 text-[9px] bg-black text-white px-1 border border-[#4a3b5e] rounded-sm z-10 shadow-sm font-mono"
|
||||||
|
>{{ getInventorySlotItem(index - 1).quantity }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Appearance Slot -->
|
<span
|
||||||
<div
|
class="text-[10px] text-center leading-tight line-clamp-2 w-full font-medium relative z-10"
|
||||||
class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center cursor-pointer hover:border-[#d584fb] group"
|
:style="{
|
||||||
@click="getEquippedItem(slot, true) && setSelectedItemId(getEquippedItem(slot, true)?.id)"
|
color: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color,
|
||||||
>
|
textShadow: ['rare', 'epic', 'legendary'].includes(getInventorySlotItem(index - 1).rarity)
|
||||||
<span class="text-[9px] text-[#8f80a0] mb-0.5">COSMETIC</span>
|
? `0 0 3px ${ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color}80`
|
||||||
<span v-if="getEquippedItem(slot, true)" class="text-xs text-center" :style="{ color: RARITY_COLORS[getEquippedItem(slot, true)!.rarity] }">{{ getEquippedItem(slot, true)!.name }}</span>
|
: 'none'
|
||||||
<span v-else class="text-[10px] text-[#4a3b5e]">Empty</span>
|
}"
|
||||||
</div>
|
>{{ getInventorySlotItem(index - 1).name }}</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 3. Backpack Section -->
|
<!-- 详细悬浮卡片 -->
|
||||||
<div class="flex-grow flex flex-col md:flex-row gap-2 overflow-hidden mt-2">
|
<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">
|
||||||
|
<!-- 物品头部 -->
|
||||||
<!-- Item Grid -->
|
<div class="flex gap-3 mb-3 pb-3 border-b border-[#4a3b5e]">
|
||||||
<PixelFrame class="flex-grow flex flex-col bg-[#1b1026]" :title="`Backpack (${items.filter(i => !i.isEquipped).length})`">
|
<div class="w-16 h-16 bg-[#1b1026] border-2 rounded flex items-center justify-center p-2 flex-shrink-0"
|
||||||
<div class="flex-grow overflow-y-auto p-1 custom-scrollbar">
|
:style="{ borderColor: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color }">
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2">
|
<PixelItemIcon
|
||||||
<button
|
:category="getInventorySlotItem(index - 1).category"
|
||||||
v-for="item in items.filter(i => !i.isEquipped)"
|
:color="ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color"
|
||||||
: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>
|
</div>
|
||||||
<span class="text-[10px] text-center leading-tight line-clamp-2" :style="{ color: RARITY_COLORS[item.rarity] }">
|
<div class="flex-1 min-w-0">
|
||||||
{{ item.name }}
|
<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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- 描述 -->
|
||||||
<div class="mb-2">
|
<p class="text-[11px] text-[#e0d8f0] italic mb-3 leading-relaxed">
|
||||||
<p class="text-xs text-[#e0d8f0] italic mb-2">"{{ selectedItem.description }}"</p>
|
"{{ getInventorySlotItem(index - 1).description }}"
|
||||||
|
</p>
|
||||||
|
|
||||||
<!-- Stats Block -->
|
<!-- 效果 -->
|
||||||
<div v-if="selectedItem.statsDescription" class="bg-[#0f0816] border border-[#4a3b5e] p-2 mb-2">
|
<div v-if="getInventorySlotItem(index - 1).effects" class="mb-3 space-y-1">
|
||||||
<span class="text-[10px] text-[#99e550] block mb-1">EFFECTS:</span>
|
<div class="text-[10px] text-[#99e550] font-bold mb-1">效果</div>
|
||||||
<span class="text-xs text-[#2ce8f4]">{{ selectedItem.statsDescription }}</span>
|
<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 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- 耐久度 -->
|
||||||
<div class="mt-auto flex flex-col gap-2">
|
<div v-if="getInventorySlotItem(index - 1).maxDurability && getInventorySlotItem(index - 1).maxDurability !== Infinity" class="mb-3">
|
||||||
<div v-if="selectedItem.type === ItemType.Equipment" class="grid grid-cols-2 gap-2">
|
<div class="flex justify-between text-[10px] text-[#8f80a0] mb-1">
|
||||||
<PixelButton
|
<span>耐久度</span>
|
||||||
class="text-[10px] py-1"
|
<span class="font-mono">{{ getInventorySlotItem(index - 1).durability }} / {{ getInventorySlotItem(index - 1).maxDurability }}</span>
|
||||||
:disabled="selectedItem.isEquipped && !selectedItem.isAppearance"
|
</div>
|
||||||
@click="$emit('equip', selectedItem.id, false)"
|
<div class="h-1.5 w-full bg-[#1b1026] border border-[#4a3b5e] rounded-full overflow-hidden">
|
||||||
>
|
<div class="h-full bg-[#f6b26b] transition-all"
|
||||||
{{ selectedItem.isEquipped && !selectedItem.isAppearance ? 'EQUIPPED' : 'EQUIP' }}
|
:style="{ width: `${(getInventorySlotItem(index - 1).durability / getInventorySlotItem(index - 1).maxDurability) * 100}%` }">
|
||||||
</PixelButton>
|
</div>
|
||||||
<PixelButton
|
</div>
|
||||||
variant="secondary"
|
|
||||||
class="text-[10px] py-1"
|
|
||||||
:disabled="selectedItem.isEquipped && selectedItem.isAppearance"
|
|
||||||
@click="$emit('equip', selectedItem.id, true)"
|
|
||||||
>
|
|
||||||
COSMETIC
|
|
||||||
</PixelButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PixelButton v-if="selectedItem.type === ItemType.Consumable" @click="$emit('use', selectedItem.id)">USE ITEM</PixelButton>
|
<!-- 操作按钮 -->
|
||||||
|
<div class="space-y-2 pt-2 border-t border-[#4a3b5e]">
|
||||||
<div class="flex justify-between mt-2 pt-2 border-t border-[#4a3b5e]">
|
<!-- 装备按钮 -->
|
||||||
<button
|
<button
|
||||||
v-if="selectedItem.isEquipped"
|
v-if="getInventorySlotItem(index - 1).slot && getInventorySlotItem(index - 1).type !== ItemType.Appearance"
|
||||||
@click="$emit('unequip', selectedItem.slot!, selectedItem.isAppearance!)"
|
@click.stop="$emit('equip', getInventorySlotItem(index - 1).id, false)"
|
||||||
class="text-[#f6b26b] text-xs hover:underline"
|
class="w-full px-3 py-2 bg-[#9fd75b] text-[#1b1026] rounded text-xs font-bold hover:bg-[#b5e87b] transition-colors"
|
||||||
>
|
>
|
||||||
Unequip
|
裝備
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- 外观按钮 -->
|
||||||
<button
|
<button
|
||||||
@click="$emit('delete', selectedItem.id)"
|
v-if="getInventorySlotItem(index - 1).type === ItemType.Appearance"
|
||||||
class="text-[#d95763] text-xs hover:text-red-400 flex items-center gap-1 ml-auto"
|
@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"
|
||||||
>
|
>
|
||||||
<Trash2 :size="10" /> Delete
|
裝備外觀
|
||||||
|
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
</PixelFrame>
|
<!-- 空格子:占位符 -->
|
||||||
<PixelFrame v-else class="h-full bg-[#150c1f] flex items-center justify-center text-[#4a3b5e]">
|
<div
|
||||||
<div class="text-center">
|
v-else
|
||||||
<HelpCircle :size="32" class="mx-auto mb-2 opacity-50" />
|
class="w-full h-full border-2 border-[#2b193f] bg-[#150c1f] rounded-sm flex items-center justify-center opacity-30"
|
||||||
<span class="text-xs">Select an item<br/>to view details</span>
|
>
|
||||||
|
<div class="w-8 h-8 border border-[#2b193f] rounded opacity-20"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PixelFrame>
|
</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>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Full rewrite with clean structure, full-width grid (6 columns), fixed scrollable height, and detailed hover tooltips with all actions
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)" />
|
||||||
|
|
||||||
<!-- Hair -->
|
<!-- Patterns -->
|
||||||
<rect x="5" y="1" width="6" height="2" :fill="finalHair" />
|
<!-- CAT Patterns -->
|
||||||
<rect x="4" y="2" width="1" height="3" :fill="finalHair" />
|
<g v-if="species === 'cat'">
|
||||||
<rect x="11" y="2" width="1" height="3" :fill="finalHair" />
|
<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>
|
||||||
|
|
||||||
<!-- Face -->
|
<!-- DOG Patterns -->
|
||||||
<rect x="7" y="4" width="1" height="1" fill="#000" opacity="0.6"/> <!-- Eye -->
|
<g v-else>
|
||||||
<rect x="9" y="4" width="1" height="1" fill="#000" opacity="0.6"/> <!-- Eye -->
|
<!-- Bone Pattern -->
|
||||||
|
<g v-if="eggPattern === 'bone'" fill="rgba(255,255,255,0.3)">
|
||||||
|
<rect x="14" y="14" width="4" height="2" />
|
||||||
|
<rect x="13" y="13" width="1" height="1" />
|
||||||
|
<rect x="13" y="16" width="1" height="1" />
|
||||||
|
<rect x="18" y="13" width="1" height="1" />
|
||||||
|
<rect x="18" y="16" width="1" height="1" />
|
||||||
|
</g>
|
||||||
|
<!-- Paw Pattern -->
|
||||||
|
<g v-else-if="eggPattern === 'paw'" fill="rgba(255,255,255,0.2)">
|
||||||
|
<rect x="15" y="18" width="2" height="2" /> <!-- Pad -->
|
||||||
|
<rect x="14" y="16" width="1" height="1" />
|
||||||
|
<rect x="16" y="15" width="1" height="1" />
|
||||||
|
<rect x="18" y="16" width="1" height="1" />
|
||||||
|
</g>
|
||||||
|
<!-- Patch Pattern -->
|
||||||
|
<g v-else-if="eggPattern === 'patch'" fill="rgba(255,255,255,0.2)">
|
||||||
|
<rect x="18" y="8" width="4" height="4" />
|
||||||
|
<rect x="10" y="20" width="6" height="4" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
<!-- Accessory/Deity Specifics -->
|
<!-- Cracks (if close to hatching) -->
|
||||||
<rect v-if="deityId === 'Mazu'" x="5" y="0" width="6" height="1" fill="#d4af37" /> <!-- Crown -->
|
<g v-if="isHatching" fill="#2b193f">
|
||||||
<rect v-if="deityId === 'EarthGod'" x="5" y="10" width="6" height="1" fill="#8e5c2e" /> <!-- Belt -->
|
<rect x="14" y="6" width="1" height="4" />
|
||||||
<rect v-if="deityId === 'Matchmaker'" x="10" y="7" width="2" height="2" fill="#ff0000" /> <!-- Red Thread -->
|
<rect x="13" y="9" width="1" height="2" />
|
||||||
|
<rect x="15" y="8" width="1" height="3" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
<!-- Weapon -->
|
<!-- DEITY STAGE -->
|
||||||
<path v-if="weapon === 'sword'" d="M11 9 L13 7 L14 8 L12 10 Z" fill="#ccc" />
|
<g v-else-if="deityId">
|
||||||
<rect v-if="weapon === 'staff'" x="11" y="5" width="1" height="8" fill="#8d6e63" />
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- Earth God (土地公) -->
|
||||||
|
<g v-else-if="deityId === 'earth_god'">
|
||||||
|
<rect x="10" y="6" width="12" height="10" fill="#f0c0a8" /> <!-- Face -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Matchmaker (月老) -->
|
||||||
|
<g v-else-if="deityId === 'matchmaker'">
|
||||||
|
<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
|
||||||
|
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;
|
return props.outfitColor;
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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' };
|
||||||
|
|
|
||||||
|
|
@ -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) => ({
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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: ['忠誠', '憨厚']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue