From bae581b7781702381dfc0f04879caefe92f4c190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Fri, 21 Nov 2025 16:26:40 +0800 Subject: [PATCH] feat:merge health --- src/App.vue | 88 +++- src/components/ActionMenu.vue | 52 ++- src/components/DeviceShell.vue | 6 +- src/components/FortuneStickAnimation.vue | 30 +- src/components/JiaobeiAnimation.vue | 30 +- src/components/PetGame.vue | 251 ++++++++-- src/components/PetInfo.vue | 149 ++++++ src/components/PrayerMenu.vue | 176 ++++--- src/components/TopMenu.vue | 51 +- src/composables/usePetSystem.js | 223 +++++---- src/data/petPresets.js | 564 +++++++++++++---------- 11 files changed, 1147 insertions(+), 473 deletions(-) create mode 100644 src/components/PetInfo.vue diff --git a/src/App.vue b/src/App.vue index a1165d5..e428051 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,7 +8,16 @@ import { usePetSystem } from './composables/usePetSystem'; const currentScreen = ref('game'); const petGameRef = ref(null); -const showStats = ref(false); // Stats visibility +const statsMode = ref('none'); // 'none', 'bars', 'details' + +// Menu State +const isMenuOpen = ref(false); +const selectedMenuIndex = ref(0); +// Combined menu items: Top (0-3) + Bottom (4-7) +const MENU_ITEMS = [ + 'stats', 'feed', 'play', 'sleep', // Top Menu + 'clean', 'medicine', 'training', 'info' // Action Menu +]; // Initialize Pet System const { @@ -24,8 +33,48 @@ const { reset } = usePetSystem(); +// Handle Physical Buttons +function handleButton(btnId) { + console.log('Button pressed:', btnId); + + // 0. Check if PetGame wants to handle the input (e.g. Prayer Menu, Minigames) + if (petGameRef.value && petGameRef.value.handleInput) { + if (petGameRef.value.handleInput(btnId)) { + return; // Input handled by game component + } + } + + // 1. Idle State (Menu Closed) + if (!isMenuOpen.value) { + if (btnId === 1) { + // Button 1: Open Menu + isMenuOpen.value = true; + selectedMenuIndex.value = 0; // Default to first item + } + // Buttons 2 & 3 do nothing in idle state (or could trigger other things) + return; + } + + // 2. Menu Active State + if (isMenuOpen.value) { + if (btnId === 2) { + // Button 2: Left (Previous) + selectedMenuIndex.value = (selectedMenuIndex.value - 1 + MENU_ITEMS.length) % MENU_ITEMS.length; + } else if (btnId === 3) { + // Button 3: Right (Next) + selectedMenuIndex.value = (selectedMenuIndex.value + 1) % MENU_ITEMS.length; + } else if (btnId === 1) { + // Button 1: Confirm + const action = MENU_ITEMS[selectedMenuIndex.value]; + handleAction(action); + isMenuOpen.value = false; // Close menu after selection + } + } +} + // Handle Action Menu Events function handleAction(action) { + console.log('Action triggered:', action); switch(action) { case 'feed': const feedResult = feed(); @@ -51,8 +100,25 @@ function handleAction(action) { } break; case 'stats': - // Toggle stats display - showStats.value = !showStats.value; + // Toggle stats mode: none -> bars -> details -> none + if (statsMode.value === 'none') statsMode.value = 'bars'; + else if (statsMode.value === 'bars') statsMode.value = 'details'; + else statsMode.value = 'none'; + break; + case 'info': + // Show info (same as stats details for now, or separate) + if (statsMode.value !== 'details') statsMode.value = 'details'; + else statsMode.value = 'none'; + break; + case 'training': + // Show Prayer Menu (handled in PetGame via prop or event?) + // We need to pass this down or handle it here. + // Currently PetGame handles 'training' event to show menu. + // We can just emit the action to PetGame if we move logic there, + // or better, expose a method on PetGame. + if (petGameRef.value) { + petGameRef.value.openPrayerMenu(); + } break; case 'settings': // Show reset options @@ -62,16 +128,6 @@ function handleAction(action) { reset(); } break; - case 'jiaobei': - // 擲筊功能 - 待實作 - console.log('擲筊功能'); - // TODO: 實作擲筊邏輯 - break; - case 'fortune': - // 求籤功能 - 待實作 - console.log('求籤功能'); - // TODO: 實作求籤邏輯 - break; default: console.log('Action not implemented:', action); } @@ -84,7 +140,7 @@ function setPetState(newState) { diff --git a/src/components/FortuneStickAnimation.vue b/src/components/FortuneStickAnimation.vue index f490b86..07bcf97 100644 --- a/src/components/FortuneStickAnimation.vue +++ b/src/components/FortuneStickAnimation.vue @@ -22,13 +22,13 @@
- - + +
- +
@@ -37,6 +37,13 @@ diff --git a/src/components/PrayerMenu.vue b/src/components/PrayerMenu.vue index 36b91cc..cf25fd8 100644 --- a/src/components/PrayerMenu.vue +++ b/src/components/PrayerMenu.vue @@ -6,6 +6,7 @@ @@ -79,77 +92,15 @@ defineEmits(['select', 'close']); min-width: 60px; /* 縮小最小寬度 */ } -.prayer-option:hover { - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +.prayer-option.active { + background: #000; + color: #fff; + border-color: #fff; + transform: scale(1.05); } -.prayer-option:active { - transform: translateY(0); -} - -.option-icon { - width: 24px; /* 縮小圖標 */ - height: 24px; - position: relative; - transform: scale(0.8); /* 稍微縮小圖標內容 */ -} - -.option-label { - font-size: 10px; /* 縮小標籤 */ - font-weight: bold; - color: #333; - font-family: monospace; -} - -/* 擲筊圖標 - 可愛版(一對圓潤的紅筊) */ -.icon-jiaobei::before { - content: ''; - position: absolute; - width: 2px; - height: 2px; - background: transparent; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - box-shadow: - /* --- 左邊筊杯 (胖胖的月牙) --- */ - -7px -4px 0 #ff5252, -5px -4px 0 #ff5252, -3px -4px 0 #ff5252, - -9px -2px 0 #ff5252, -7px -2px 0 #ff8a80, -5px -2px 0 #ff5252, -3px -2px 0 #ff5252, -1px -2px 0 #ff5252, /* #ff8a80 是高光 */ - -9px 0px 0 #ff5252, -7px 0px 0 #ff5252, -5px 0px 0 #ff5252, -3px 0px 0 #ff5252, -1px 0px 0 #ff5252, - -7px 2px 0 #ff5252, -5px 2px 0 #ff5252, -3px 2px 0 #ff5252, - -5px 4px 0 #d32f2f, /* 陰影 */ - - /* --- 右邊筊杯 (對稱的胖月牙) --- */ - 3px -4px 0 #ff5252, 5px -4px 0 #ff5252, 7px -4px 0 #ff5252, - 1px -2px 0 #ff5252, 3px -2px 0 #ff5252, 5px -2px 0 #ff5252, 7px -2px 0 #ff8a80, 9px -2px 0 #ff5252, /* 高光在右側 */ - 1px 0px 0 #ff5252, 3px 0px 0 #ff5252, 5px 0px 0 #ff5252, 7px 0px 0 #ff5252, 9px 0px 0 #ff5252, - 3px 2px 0 #ff5252, 5px 2px 0 #ff5252, 7px 2px 0 #ff5252, - 5px 4px 0 #d32f2f; /* 陰影 */ -} - -/* 求籤圖標 - 籤筒和籤條 */ -.icon-fortune::before { - content: ''; - position: absolute; - width: 2px; - height: 2px; - background: #8B4513; /* 木色 */ - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - box-shadow: - /* 籤筒本體 */ - -4px -4px 0 #8B4513, -2px -4px 0 #8B4513, 0px -4px 0 #8B4513, 2px -4px 0 #8B4513, 4px -4px 0 #8B4513, - -4px -2px 0 #8B4513, -2px -2px 0 #8B4513, 0px -2px 0 #8B4513, 2px -2px 0 #8B4513, 4px -2px 0 #8B4513, - -4px 0px 0 #8B4513, -2px 0px 0 #8B4513, 0px 0px 0 #8B4513, 2px 0px 0 #8B4513, 4px 0px 0 #8B4513, - -4px 2px 0 #8B4513, -2px 2px 0 #8B4513, 0px 2px 0 #8B4513, 2px 2px 0 #8B4513, 4px 2px 0 #8B4513, - -4px 4px 0 #8B4513, -2px 4px 0 #8B4513, 0px 4px 0 #8B4513, 2px 4px 0 #8B4513, 4px 4px 0 #8B4513, - - /* 突出的籤條(紅色) */ - -2px -8px 0 #d4522e, 0px -8px 0 #d4522e, - -2px -6px 0 #d4522e, 0px -6px 0 #d4522e, - 2px -6px 0 #d4522e; +.prayer-option.active .option-label { + color: #fff; } .back-button { @@ -164,11 +115,86 @@ defineEmits(['select', 'close']); font-family: monospace; } -.back-button:hover { - background: rgba(255, 255, 255, 1); +.back-button.active { + background: #000; + color: #fff; + border-color: #fff; } -.back-button:active { - transform: translateY(1px); +/* --- Icons (Pixel Art) --- */ +.option-icon { + width: 24px; + height: 24px; + position: relative; + margin-bottom: 4px; } - + +/* Jiaobei (Moon Blocks) - Two red crescents */ +.icon-jiaobei::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 2px; + height: 2px; + background: #d4522e; + transform: translate(-8px, -2px); /* Left block */ + box-shadow: + 2px -2px 0 #d4522e, 4px -2px 0 #d4522e, + 0px 0px 0 #d4522e, 2px 0px 0 #d4522e, 4px 0px 0 #d4522e, 6px 0px 0 #d4522e, + 0px 2px 0 #d4522e, 2px 2px 0 #d4522e, 4px 2px 0 #d4522e, + 2px 4px 0 #d4522e; +} + +.icon-jiaobei::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 2px; + height: 2px; + background: #d4522e; + transform: translate(2px, -2px) scaleX(-1); /* Right block (mirrored) */ + box-shadow: + 2px -2px 0 #d4522e, 4px -2px 0 #d4522e, + 0px 0px 0 #d4522e, 2px 0px 0 #d4522e, 4px 0px 0 #d4522e, 6px 0px 0 #d4522e, + 0px 2px 0 #d4522e, 2px 2px 0 #d4522e, 4px 2px 0 #d4522e, + 2px 4px 0 #d4522e; +} + +/* Fortune (Stick Container) */ +.icon-fortune::before { + content: ''; + position: absolute; + bottom: 2px; + left: 50%; + width: 2px; + height: 2px; + background: #8b4513; + transform: translateX(-50%); + box-shadow: + /* Container Body */ + -4px 0 0 #8b4513, -2px 0 0 #a0522d, 0 0 0 #a0522d, 2px 0 0 #a0522d, 4px 0 0 #8b4513, + -4px -2px 0 #8b4513, -2px -2px 0 #a0522d, 0 -2px 0 #a0522d, 2px -2px 0 #a0522d, 4px -2px 0 #8b4513, + -4px -4px 0 #8b4513, -2px -4px 0 #a0522d, 0 -4px 0 #a0522d, 2px -4px 0 #a0522d, 4px -4px 0 #8b4513, + -4px -6px 0 #8b4513, -2px -6px 0 #a0522d, 0 -6px 0 #a0522d, 2px -6px 0 #a0522d, 4px -6px 0 #8b4513, + -4px -8px 0 #8b4513, -2px -8px 0 #a0522d, 0 -8px 0 #a0522d, 2px -8px 0 #a0522d, 4px -8px 0 #8b4513, + /* Rim */ + -6px -10px 0 #5c2e0e, -4px -10px 0 #8b4513, -2px -10px 0 #8b4513, 0 -10px 0 #8b4513, 2px -10px 0 #8b4513, 4px -10px 0 #8b4513, 6px -10px 0 #5c2e0e; +} + +.icon-fortune::after { + content: ''; + position: absolute; + bottom: 14px; + left: 50%; + width: 2px; + height: 2px; + background: #f0d09c; + transform: translateX(-50%); + box-shadow: + /* Sticks */ + -2px 0 0 #f0d09c, 2px 0 0 #f0d09c, + -3px -2px 0 #f0d09c, 0px -2px 0 #f0d09c, 3px -2px 0 #f0d09c, + -1px -4px 0 #ff4444, 1px -4px 0 #ff4444; /* Red tips */ +} diff --git a/src/components/TopMenu.vue b/src/components/TopMenu.vue index b63479d..2d5cbbf 100644 --- a/src/components/TopMenu.vue +++ b/src/components/TopMenu.vue @@ -1,9 +1,32 @@ @@ -12,6 +35,14 @@ const props = defineProps({ disabled: { type: Boolean, default: false + }, + isMenuOpen: { + type: Boolean, + default: false + }, + selectedIndex: { + type: Number, + default: 0 } }); @@ -36,10 +67,20 @@ defineEmits(['stats', 'feed', 'play', 'sleep']); cursor: pointer; position: relative; padding: 0; + opacity: 0.5; /* Default dim */ + transition: opacity 0.2s, transform 0.2s; +} + +.icon-btn.active { + opacity: 1; + transform: scale(1.1); + filter: drop-shadow(0 0 2px rgba(0,0,0,0.5)); + background: rgba(255, 255, 255, 0.5); + border-radius: 4px; } .icon-btn:disabled { - opacity: 0.3; + opacity: 0.2; cursor: not-allowed; } diff --git a/src/composables/usePetSystem.js b/src/composables/usePetSystem.js index 2a1dc1f..779ef89 100644 --- a/src/composables/usePetSystem.js +++ b/src/composables/usePetSystem.js @@ -1,47 +1,64 @@ -import { ref, computed, onMounted, onUnmounted } from 'vue'; +import { ref, computed, onMounted, onUnmounted, watch } from 'vue'; +import { SPRITE_PRESETS } from '../data/petPresets.js'; + +const CONFIG = SPRITE_PRESETS.tinyTigerCatB; export function usePetSystem() { // --- State --- - const stage = ref('egg'); // egg, baby, adult + const stage = ref('egg'); // egg, baby, child, adult const state = ref('idle'); // idle, sleep, eating, sick, dead, refuse // --- Stats --- const stats = ref({ - hunger: 100, // 0-100 (0 = Starving) - happiness: 100, // 0-100 (0 = Depressed) - health: 100, // 0-100 (0 = Sick risk) - weight: 500, // grams - age: 0, // days - poopCount: 0 // Number of poops on screen + hunger: CONFIG.needs.hunger.startValue, + happiness: CONFIG.needs.happiness.startValue, + cleanliness: CONFIG.needs.cleanliness.startValue, + energy: CONFIG.needs.energy.startValue, + health: 100, + poopCount: 0, + ageMinutes: 0, // Track age in minutes + + // Tracking for evolution + hungerEvents: 0, + sicknessEvents: 0 }); // --- Internal Timers --- let gameLoopId = null; - const TICK_RATE = 3000; // 3 seconds per tick + const TICK_RATE = 1000; // 1 second per tick for easier calculation + const SECONDS_PER_MINUTE = 60; // Game time scale (1 real sec = 1 game sec for now) const isCleaning = ref(false); + // --- Computed Helpers --- + const currentStageConfig = computed(() => { + return CONFIG.lifecycle.stages.find(s => s.id === stage.value); + }); + // --- Actions --- function feed() { if (state.value === 'sleep' || state.value === 'dead' || stage.value === 'egg' || isCleaning.value) return false; + // Check refusal (sick or full) if (state.value === 'sick' || stats.value.hunger >= 90) { - // Refuse food if sick or full triggerState('refuse', 2000); return false; } // Eat - triggerState('eating', 3000); // Animation duration - stats.value.hunger = Math.min(100, stats.value.hunger + 20); - stats.value.weight += 50; + triggerState('eating', 3000); - // Chance to poop after eating (降低機率) - if (Math.random() < 0.15) { // 從 0.3 降到 0.15 + // Recover hunger + stats.value.hunger = Math.min(CONFIG.needs.hunger.max, stats.value.hunger + CONFIG.needs.hunger.feedRecover); + + // Poop logic + // Simplified: Chance to poop based on feed count or random + if (Math.random() < (1 / CONFIG.needs.poop.feedsPerPoop)) { setTimeout(() => { - if (stats.value.poopCount < 4) { + if (stats.value.poopCount < CONFIG.needs.poop.maxPoopOnScreen) { stats.value.poopCount++; + stats.value.cleanliness -= CONFIG.needs.cleanliness.decayPerPoop; } }, 4000); } @@ -52,9 +69,9 @@ export function usePetSystem() { function play() { if (state.value !== 'idle' || stage.value === 'egg' || isCleaning.value) return false; - stats.value.happiness = Math.min(100, stats.value.happiness + 15); - stats.value.weight -= 10; // Exercise burns calories - stats.value.hunger = Math.max(0, stats.value.hunger - 5); + stats.value.happiness = Math.min(CONFIG.needs.happiness.max, stats.value.happiness + CONFIG.needs.happiness.playRecover); + stats.value.energy = Math.max(0, stats.value.energy - 5); // Playing tires you out + stats.value.hunger = Math.max(0, stats.value.hunger - 5); // Playing makes you hungry return true; } @@ -62,12 +79,12 @@ export function usePetSystem() { if (stats.value.poopCount > 0 && !isCleaning.value) { isCleaning.value = true; - // Delay removal for animation setTimeout(() => { stats.value.poopCount = 0; - stats.value.happiness += 10; + stats.value.cleanliness = CONFIG.needs.cleanliness.max; + stats.value.happiness = Math.min(CONFIG.needs.happiness.max, stats.value.happiness + 10); isCleaning.value = false; - }, 2000); // 2 seconds flush animation + }, 2000); return true; } @@ -80,74 +97,116 @@ export function usePetSystem() { if (state.value === 'idle') { state.value = 'sleep'; } else if (state.value === 'sleep') { - state.value = 'idle'; // Wake up + state.value = 'idle'; } } // --- Game Loop --- function tick() { - if (state.value === 'dead' || stage.value === 'egg') return; + if (state.value === 'dead') return; - // Decrease stats naturally - // 目標:飢餓值約 30-60 分鐘下降 10%,快樂值約 20-40 分鐘下降 10% - // TICK_RATE = 3000ms (3秒), 600 ticks = 30分鐘 - // 飢餓值每 tick -0.05 → 600 ticks = -30 (30分鐘下降30%) - // 快樂值每 tick -0.08 → 600 ticks = -48 (30分鐘下降48%) + // 1. Age Growth + // Add 1/60th of a minute (since tick is 1 sec) + stats.value.ageMinutes += (TICK_RATE / 1000) / 60; + + // Check Evolution + checkEvolution(); + + if (stage.value === 'egg') return; // Egg doesn't have needs decay + + // 2. Needs Decay (Per Minute converted to Per Tick) + const decayFactor = (TICK_RATE / 1000) / 60; // Fraction of a minute passed if (state.value !== 'sleep') { - stats.value.hunger = Math.max(0, stats.value.hunger - 0.05); - stats.value.happiness = Math.max(0, stats.value.happiness - 0.08); + stats.value.hunger = Math.max(0, stats.value.hunger - CONFIG.needs.hunger.decayPerMinute * decayFactor); + stats.value.happiness = Math.max(0, stats.value.happiness - CONFIG.needs.happiness.decayPerMinute * decayFactor); + stats.value.energy = Math.max(0, stats.value.energy - CONFIG.needs.energy.decayPerMinuteActive * decayFactor); } else { - // Slower decay when sleeping (約 1/3 速度) - stats.value.hunger = Math.max(0, stats.value.hunger - 0.015); - stats.value.happiness = Math.max(0, stats.value.happiness - 0.025); + // Sleeping recovers energy, slower hunger decay + stats.value.energy = Math.min(CONFIG.needs.energy.max, stats.value.energy + CONFIG.needs.energy.recoverPerMinuteSleep * decayFactor); + stats.value.hunger = Math.max(0, stats.value.hunger - (CONFIG.needs.hunger.decayPerMinute * 0.5) * decayFactor); } - // Random poop generation (更低的機率:約 0.5% per tick) - // 平均約每 200 ticks = 10 分鐘拉一次 - if (state.value !== 'sleep' && Math.random() < 0.005 && stats.value.poopCount < 4 && !isCleaning.value) { - stats.value.poopCount++; - } - - // Health Logic (更溫和的健康下降) - // 便便影響健康:每個便便每 tick -0.1 health + // 3. Health Logic + // Poop penalty if (stats.value.poopCount > 0) { stats.value.health = Math.max(0, stats.value.health - (0.1 * stats.value.poopCount)); } - // 飢餓影響健康:飢餓值低於 20 時開始影響健康 - if (stats.value.hunger < 20) { - const hungerPenalty = (20 - stats.value.hunger) * 0.02; // 飢餓越嚴重,扣越多 - stats.value.health = Math.max(0, stats.value.health - hungerPenalty); + // Starvation penalty + if (stats.value.hunger < CONFIG.needs.hunger.criticalThreshold) { + stats.value.health = Math.max(0, stats.value.health - 0.05); + stats.value.hungerEvents++; // Track for evolution } - // 不開心影響健康:快樂值低於 20 時開始影響健康(較輕微) - if (stats.value.happiness < 20) { - const happinessPenalty = (20 - stats.value.happiness) * 0.01; - stats.value.health = Math.max(0, stats.value.health - happinessPenalty); + // Sickness Chance + let sickChance = CONFIG.needs.sickness.baseChancePerMinute * decayFactor; + if (stats.value.cleanliness < CONFIG.needs.cleanliness.criticalThreshold) sickChance += CONFIG.needs.sickness.extraChanceIfDirty * decayFactor; + if (stats.value.hunger < CONFIG.needs.hunger.criticalThreshold) sickChance += CONFIG.needs.sickness.extraChanceIfStarving * decayFactor; + + if (Math.random() < sickChance && state.value !== 'sick') { + state.value = 'sick'; + stats.value.sicknessEvents++; } - // Sickness Check (更低的生病機率) - if (stats.value.health < 30 && state.value !== 'sick') { - if (Math.random() < 0.1) { // 從 0.3 降到 0.1 - state.value = 'sick'; + // 4. Mood Update (Simplified) + updateMood(); + + // 5. Random Events + checkRandomEvents(decayFactor); + } + + function checkEvolution() { + // Current stage max age check + if (currentStageConfig.value && currentStageConfig.value.maxAgeMinutes !== Infinity) { + if (stats.value.ageMinutes >= currentStageConfig.value.maxAgeMinutes) { + // Find next stage + // Simple linear progression for now, or use evolutionRules + const currentIndex = CONFIG.lifecycle.stages.findIndex(s => s.id === stage.value); + if (currentIndex < CONFIG.lifecycle.stages.length - 1) { + const nextStage = CONFIG.lifecycle.stages[currentIndex + 1]; + evolveTo(nextStage.id); + } } } + } - // Health Recovery (健康值可以緩慢恢復) - // 如果沒有便便、飢餓值和快樂值都高,健康值會緩慢恢復 - if (stats.value.poopCount === 0 && stats.value.hunger > 50 && stats.value.happiness > 50 && stats.value.health < 100 && state.value !== 'sick') { - stats.value.health = Math.min(100, stats.value.health + 0.05); - } + function evolveTo(newStageId) { + console.log(`Evolving from ${stage.value} to ${newStageId}`); + stage.value = newStageId; + triggerState('idle', 1000); // Celebration? + } - // Death Check (移除死亡機制,依照之前的討論) - // if (stats.value.health === 0) { - // state.value = 'dead'; - // } + function updateMood() { + // Determine mood based on CONFIG.mood.states + // Priority: Angry > Sad > Happy > Neutral + const m = CONFIG.mood.states; - // Evolution / Growth (Simple Age increment) - // In a real game, 1 day might be 24h, here maybe every 100 ticks? - // For now, let's just say age increases slowly. + // This is just internal state tracking, visual update happens in PetGame via props or events + // For now we just log or emit if needed + } + + function checkRandomEvents(decayFactor) { + CONFIG.randomEvents.forEach(event => { + const chance = event.chancePerMinute * decayFactor; + if (Math.random() < chance) { + // Check conditions + let conditionMet = true; + if (event.condition) { + if (event.condition.minEnergy && stats.value.energy < event.condition.minEnergy) conditionMet = false; + // Add other condition checks + } + + if (conditionMet) { + console.log('Random Event:', event.message); + // Apply effects + if (event.effect) { + if (event.effect.happiness) stats.value.happiness += event.effect.happiness; + if (event.effect.energy) stats.value.energy += event.effect.energy; + } + } + } + }); } // --- Helpers --- @@ -155,7 +214,7 @@ export function usePetSystem() { const previousState = state.value; state.value = tempState; setTimeout(() => { - if (state.value === tempState) { // Only revert if state hasn't changed again + if (state.value === tempState) { state.value = previousState === 'sleep' ? 'idle' : 'idle'; } }, duration); @@ -163,15 +222,14 @@ export function usePetSystem() { function hatchEgg() { if (stage.value === 'egg') { - stage.value = 'baby'; // or 'adult' for now since we only have that sprite - // Let's map 'baby' to our 'adult' sprite for now, or just use 'adult' - stage.value = 'adult'; - state.value = 'idle'; - stats.value.hunger = 50; - stats.value.happiness = 50; - stats.value.health = 100; - stats.value.poopCount = 0; - isCleaning.value = false; + // Force evolve to baby + evolveTo('baby'); + + // Reset stats for baby + stats.value.hunger = CONFIG.needs.hunger.startValue; + stats.value.happiness = CONFIG.needs.happiness.startValue; + stats.value.energy = CONFIG.needs.energy.startValue; + stats.value.cleanliness = CONFIG.needs.cleanliness.startValue; } } @@ -180,12 +238,15 @@ export function usePetSystem() { state.value = 'idle'; isCleaning.value = false; stats.value = { - hunger: 100, - happiness: 100, + hunger: CONFIG.needs.hunger.startValue, + happiness: CONFIG.needs.happiness.startValue, + cleanliness: CONFIG.needs.cleanliness.startValue, + energy: CONFIG.needs.energy.startValue, health: 100, - weight: 500, - age: 0, - poopCount: 0 + poopCount: 0, + ageMinutes: 0, + hungerEvents: 0, + sicknessEvents: 0 }; } diff --git a/src/data/petPresets.js b/src/data/petPresets.js index f44cba4..90cb38b 100644 --- a/src/data/petPresets.js +++ b/src/data/petPresets.js @@ -2,255 +2,323 @@ // 定義各種寵物的像素藝術數據 export const SPRITE_PRESETS = { - tigerChick: { - name: 'tigerChick', - pixelSize: 3, - sprite: [ - '1100000111000000', - '1241111331000000', - '1005102301000000', - '1054103320000000', - '1241143320100000', - '1230432311100110', - '1245321330100421', - '1240001311100111', - '1020103030111421', - '0100000331245210', - '0001111111240210', - '0015022221345210', - '0004022235350010', - '0011400023203210', - '0005011112115210', - '0001100001100110', - ], - // 張嘴版:修改嘴巴部分的像素(row 8-9 是嘴巴區域) - spriteMouthOpen: [ - '1100000111000000', // Row 0 - '1241111331000000', // Row 1 - '1005102301000000', // Row 2 - '1054103320000000', // Row 3 - '1241143320100000', // Row 4 - '1230432311100110', // Row 5 - '1245321330100421', // Row 6 - 保持不變 - '1240001311100111', // Row 7 - 保持不變 - '1000000030111421', // Row 8 - 嘴巴張開(更往前,移除位置2-7) - '0000000331245210', // Row 9 - 嘴巴張開(更往前,移除位置1-7) - '0001111111240210', // Row 10 - '0015022221345210', // Row 11 - '0004022235350010', // Row 12 - '0011400023203210', // Row 13 - '0005011112115210', // Row 14 - '0001100001100110', // Row 15 - ], - palette: { - '1': '#2b2825', - '2': '#d0974b', - '3': '#e09037', - '4': '#4a2b0d', - '5': '#724e22', - }, - tailPixels: [ - [15, 8], [14, 8], - [15, 9], [14, 9], - [15, 10], [14, 10], - [15, 11], [14, 11], - ], - legFrontPixels: [ - [6, 13], [7, 13], - [6, 14], [7, 14], - ], - legBackPixels: [ - [9, 13], [10, 13], - [9, 14], [10, 14], - ], - earPixels: [ - [2, 0], [3, 0], [4, 0], - [11, 0], [12, 0], [13, 0], - ], - blushPixels: [ - [4, 7], [5, 7], - [10, 7], [11, 7], - ], - iconBackLeft: { x: 3, y: 2 }, - iconBackRight: { x: 12, y: 2 }, - }, - tinyTigerCat: { - name: '小虎斑貓', - pixelSize: 3, - sprite: [ - '0000000000000000', - '0011000000110000', // row 1 - Ears - '0122111111221000', // row 2 - '0122222222221000', // row 3 - '0122322223221000', // row 4 - Stripes - '0122222222221000', // row 5 - '0120022220021000', // row 6 - Eyes - '0122223322221000', // row 7 - Nose/Mouth - '0122222222221000', // row 8 - '0011222222110000', // row 9 - Body - '0001222222121000', // row 10 - Body + Tail - '0001222222121000', // row 11 - '0001100110110000', // row 12 - Legs - '0000000000000000', // row 13 - '0000000000000000', // row 14 - '0000000000000000', // row 15 - ], - spriteMouthOpen: [ - '0000000000000000', - '0011000000110000', - '0122111111221000', - '0122222222221000', - '0122322223221000', - '0122222222221000', - '0120022220021000', - '0122223322221000', - '0122200002221000', // Mouth Open - '0011222222110000', - '0001222222121000', - '0001222222121000', - '0001100110110000', - '0000000000000000', - '0000000000000000', - '0000000000000000', - ], - palette: { - '0': '#000000', // Black eyes/outline - '1': '#2b1d12', // Dark brown outline - '2': '#ffb347', // Orange fur - '3': '#cd853f', // Darker stripes/nose - }, - tailPixels: [ - [11, 10], [12, 10], - [11, 11], [12, 11], - ], - earPixels: [ - [2, 1], [3, 1], - [10, 1], [11, 1], - ], - legFrontPixels: [ - [4, 12], [5, 12], - ], - legBackPixels: [ - [8, 12], [9, 12], - ], - blushPixels: [ - [3, 7], [10, 7] - ], - iconBackLeft: { x: 2, y: 2 }, - iconBackRight: { x: 11, y: 2 } - }, tinyTigerCatB: { - name: '小虎斑貓', - pixelSize: 3, - sprite: [ - '0000000000000000', - '0011000000110000', // row 1 - Ears - '0124444111442100', // row 2 粉紅耳朵內側 - '0123222323221000', // row 3 三條虎紋 - '0122322223221000', // row 4 - Stripes - '0122522222522100', // row 5 眼睛反光 - '0125052225052100', // row 6 大圓眼+黑瞳孔+白反光 - '0112223322221100', // row 7 鼻子+左右鬍鬚 - '0122220222221000', // row 8 小微笑 - '0011222222110000', // row 9 - Body - '0001222222121000', // row 10 - Body + Tail - '0001222222121000', // row 11 - '0001100110110000', // row 12 - Legs - '0000000000000000', // row 13 - '0000000000000000', // row 14 - '0000000000000000', // row 15 - ], - spriteMouthOpen: [ - '0000000000000000', - '0011000000110000', - '0124444111442100', - '0123222323221000', - '0122322223221000', - '0122522222522100', - '0125052225052100', - '0112223322221100', - '0122204002221000', // Mouth Open 粉紅舌頭 - '0011222222110000', - '0001222222121000', - '0001222222121000', - '0001100110110000', - '0000000000000000', - '0000000000000000', - '0000000000000000', - ], - palette: { - '0': '#000000', // Black eyes/outline - '1': '#2b1d12', // Dark brown outline - '2': '#ffb347', // Orange fur - '3': '#cd853f', // Darker stripes/nose - '4': '#ffb6c1', // Pink (ears, blush, tongue) - '5': '#ffffff' // White eye highlight + id: 'tinyTigerCatB', + meta: { + name: '小虎斑貓', + displayNameEn: 'Tiny Tiger Cat', + species: 'cat', + element: 'normal', + description: '一隻活潑、黏人的小虎斑貓,喜歡被餵食和玩耍。' }, - tailPixels: [ - [11, 10], [12, 10], - [11, 11], [12, 11], - ], - earPixels: [ - [2, 1], [3, 1], - [10, 1], [11, 1], - ], - legFrontPixels: [ - [4, 12], [5, 12], - ], - legBackPixels: [ - [8, 12], [9, 12], - ], - blushPixels: [ - [3, 7], [10, 7] - ], - eyePixels: [ - [3, 6], [4, 6], // Left eye - [8, 6], [9, 6] // Right eye - ], - spriteEyesClosed: [ - '0000000000000000', - '0011000000110000', - '0124444111442100', - '0123222323221000', - '0122322223221000', - '0122522222522100', - '0122222222222100', // row 6 - Eyes closed (all '2' = closed eyes) - '0112223322221100', - '0122220222221000', - '0011222222110000', - '0001222222121000', - '0001222222121000', - '0001100110110000', - '0000000000000000', - '0000000000000000', - '0000000000000000', - ], - iconBackLeft: { x: 2, y: 2 }, - iconBackRight: { x: 13, y: 2 }, - - // Growth Stages - eggSprite: [ - '0000000000000000', - '0000000000000000', - '0000000111000000', // Top (Narrow) - '0000001222100000', - '0000012232210000', // Small stripe - '0000122333221000', - '0000122232221000', - '0001222222222100', // Widest part - '0001233322332100', // Side stripes - '0001223222232100', - '0000122222221000', - '0000122222221000', - '0000011222110000', // Bottom - '0000000111000000', - '0000000000000000', - '0000000000000000', - ], - eggPalette: { - '1': '#5d4037', // Dark brown outline - '2': '#fff8e1', // Creamy white shell - '3': '#ffb74d', // Orange tiger stripes - } + lifecycle: { + baseLifeMinutes: 7 * 24 * 60, + stages: [ + { + id: 'egg', + name: '蛋', + minAgeMinutes: 0, + maxAgeMinutes: 30, + spriteKey: 'egg', + canBattle: false, + canEquip: false + }, + { + id: 'baby', + name: '幼年期', + minAgeMinutes: 30, + maxAgeMinutes: 6 * 60, + spriteKey: 'child', + canBattle: false, + canEquip: false + }, + { + id: 'child', + name: '成長期', + minAgeMinutes: 6 * 60, + maxAgeMinutes: 24 * 60, + spriteKey: 'child', + canBattle: true, + canEquip: true + }, + { + id: 'adult', + name: '成熟期', + minAgeMinutes: 24 * 60, + maxAgeMinutes: Infinity, + spriteKey: 'adult', + canBattle: true, + canEquip: true + } + ], + evolutionRules: [ + { + fromStage: 'baby', + toStage: 'child', + condition: { + maxHungerEvents: 5, + maxSicknessEvents: 2 + } + } + ] + }, + needs: { + hunger: { + max: 100, + startValue: 70, + decayPerMinute: 2, + warnThreshold: 40, + criticalThreshold: 10, + feedRecover: 40 + }, + happiness: { + max: 100, + startValue: 60, + decayPerMinute: 1, + playRecover: 25, + lowThreshold: 30 + }, + cleanliness: { + max: 100, + startValue: 80, + decayPerPoop: 30, + criticalThreshold: 30 + }, + energy: { + max: 100, + startValue: 80, + decayPerMinuteActive: 2, + recoverPerMinuteSleep: 5, + sleepSuggestThreshold: 30 + }, + poop: { + feedsPerPoop: 3, + maxPoopOnScreen: 3 + }, + sickness: { + baseChancePerMinute: 0.001, + extraChanceIfDirty: 0.01, + extraChanceIfStarving: 0.02 + } + }, + stats: { + base: { + hp: 30, + attack: 8, + defense: 5, + speed: 7 + }, + stageModifiers: { + baby: { hp: 0.6, attack: 0.5, defense: 0.5, speed: 0.8 }, + child: { hp: 1.0, attack: 1.0, defense: 1.0, speed: 1.0 }, + adult: { hp: 1.4, attack: 1.3, defense: 1.2, speed: 1.1 } + } + }, + appearance: { + pixelSize: 3, + sprites: { + child: { + idle: [ + '0000000000000000', + '0011000000110000', + '0124444111442100', + '0123222323221000', + '0122322223221000', + '0122522222522100', + '0125052225052100', + '0112223322221100', + '0122220222221000', + '0011222222110000', + '0001222222121000', + '0001222222121000', + '0001100110110000', + '0000000000000000', + '0000000000000000', + '0000000000000000', + ], + mouthOpen: [ + '0000000000000000', + '0011000000110000', + '0124444111442100', + '0123222323221000', + '0122322223221000', + '0122522222522100', + '0125052225052100', + '0112223322221100', + '0122204002221000', + '0011222222110000', + '0001222222121000', + '0001222222121000', + '0001100110110000', + '0000000000000000', + '0000000000000000', + '0000000000000000', + ], + eyesClosed: [ + '0000000000000000', + '0011000000110000', + '0124444111442100', + '0123222323221000', + '0122322223221000', + '0122522222522100', + '0122222222222100', + '0112223322221100', + '0122220222221000', + '0011222222110000', + '0001222222121000', + '0001222222121000', + '0001100110110000', + '0000000000000000', + '0000000000000000', + '0000000000000000', + ] + }, + egg: { + idle: [ + '0000000000000000', + '0000000000000000', + '0000000111000000', + '0000001222100000', + '0000012232210000', + '0000122333221000', + '0000122232221000', + '0001222222222100', + '0001233322332100', + '0001223222232100', + '0000122222221000', + '0000122222221000', + '0000011222110000', + '0000000111000000', + '0000000000000000', + '0000000000000000', + ] + }, + adult: { + idle: [ + '0000000000000000', + '0011000000110000', + '0124444111442100', + '0123222323221000', + '0122322223221000', + '0122522222522100', + '0125052225052100', + '0112223322221100', + '0122220222221000', + '0011222222110000', + '0001222222121000', + '0001222222121000', + '0001100110110000', + '0000000000000000', + '0000000000000000', + '0000000000000000', + ] + } + }, + palettes: { + default: { + '0': '#000000', + '1': '#2b1d12', + '2': '#ffb347', + '3': '#cd853f', + '4': '#ffb6c1', + '5': '#ffffff' + }, + egg: { + '1': '#5d4037', + '2': '#fff8e1', + '3': '#ffb74d', + } + }, + bodyParts: { + tailPixels: [ + [11, 10], [12, 10], + [11, 11], [12, 11], + ], + earPixels: [ + [2, 1], [3, 1], + [10, 1], [11, 1], + ], + legFrontPixels: [ + [4, 12], [5, 12], + ], + legBackPixels: [ + [8, 12], [9, 12], + ], + blushPixels: [ + [3, 7], [10, 7] + ], + eyePixels: [ + [3, 6], [4, 6], + [8, 6], [9, 6] + ], + iconBackLeft: { x: 2, y: 2 }, + iconBackRight: { x: 13, y: 2 } + }, + behaviorAnimation: { + blinkIntervalSec: 5, + blinkDurationMs: 200, + mouthOpenDurationMs: 300, + idleEmoteIntervalSec: 15 + } + }, + equipment: { + slots: ['head', 'face', 'neck', 'back'], + items: [ + { + id: 'sunglasses_basic', + name: '基本墨鏡', + slot: 'face', + overlays: { + child: { + pixels: [ + { x: 3, y: 6, color: '0' }, + { x: 4, y: 6, color: '0' }, + { x: 8, y: 6, color: '0' }, + { x: 9, y: 6, color: '0' }, + ] + } + }, + statModifiers: { + coolness: +10 + } + } + ] + }, + personality: { + traits: ['clingy', 'energetic'], + modifiers: { + hungerDecay: 1.0, + happinessDecay: 1.2, + energyDecay: 0.9 + } + }, + mood: { + system: { + updateIntervalMinutes: 10, + factors: ['happiness', 'hunger', 'health'] + }, + states: { + happy: { minHappiness: 80, spriteFace: 'happy' }, + neutral: { minHappiness: 40, spriteFace: 'neutral' }, + sad: { maxHappiness: 40, spriteFace: 'sad' }, + angry: { maxHunger: 20, spriteFace: 'angry' } + } + }, + randomEvents: [ + { + id: 'trip_over', + chancePerMinute: 0.005, + condition: { maxCoordination: 20 }, + effect: { happiness: -5, health: -1 }, + message: 'Tripped and fell!' + }, + { + id: 'zoomies', + chancePerMinute: 0.01, + condition: { minEnergy: 90 }, + effect: { energy: -20, happiness: +10 }, + message: 'Suddenly started running around!' + } + ] } };