diff --git a/src/App.vue b/src/App.vue index a1165d5..fb1ebbb 100644 --- a/src/App.vue +++ b/src/App.vue @@ -9,6 +9,7 @@ import { usePetSystem } from './composables/usePetSystem'; const currentScreen = ref('game'); const petGameRef = ref(null); const showStats = ref(false); // Stats visibility +const debugAction = ref(null); // For passing debug commands to PetGame // Initialize Pet System const { @@ -21,7 +22,9 @@ const { clean, isCleaning, hatchEgg, - reset + reset, + achievements, + unlockAllAchievements } = usePetSystem(); // Handle Action Menu Events @@ -38,7 +41,11 @@ function handleAction(action) { clean(); break; case 'play': - play(); + if (play()) { + if (petGameRef.value) { + petGameRef.value.startPlaying(); + } + } break; case 'sleep': sleep(); @@ -81,6 +88,10 @@ function handleAction(action) { function setPetState(newState) { state.value = newState; } + +function triggerDebugAction(action, payload = null) { + debugAction.value = { type: action, payload, timestamp: Date.now() }; +} diff --git a/src/components/ActionMenu.vue b/src/components/ActionMenu.vue index 3cac302..c0d5dc9 100644 --- a/src/components/ActionMenu.vue +++ b/src/components/ActionMenu.vue @@ -140,4 +140,6 @@ defineEmits(['clean', 'medicine', 'training', 'inventory']); /* Bottom */ -2px 6px 0 #8d6e63, 0px 6px 0 #8d6e63, 2px 6px 0 #8d6e63; } + + diff --git a/src/components/InventoryScreen.vue b/src/components/InventoryScreen.vue index e7079e2..2bffcdd 100644 --- a/src/components/InventoryScreen.vue +++ b/src/components/InventoryScreen.vue @@ -21,6 +21,7 @@ @@ -54,11 +55,11 @@ import { ref, computed } from 'vue'; const props = defineProps({ inventory: { type: Array, - default: () => [ - { id: 'cookie', name: '幸運餅乾', description: '增加一點快樂值', count: 5, iconClass: 'icon-cookie' }, - { id: 'water', name: '神水', description: '恢復健康', count: 2, iconClass: 'icon-water' }, - { id: 'amulet', name: '平安符', description: '保佑寵物平安', count: 1, iconClass: 'icon-amulet' } - ] + default: () => [] + }, + equippedItems: { + type: Array, + default: () => [] } }); @@ -131,6 +132,10 @@ function useItem() { } } +function isEquipped(item) { + return props.equippedItems.includes(item.id); +} + // Tooltip Logic const hoveredItem = ref(null); const tooltipStyle = ref({ top: '0px', left: '0px' }); @@ -282,6 +287,19 @@ function updateTooltipPosition(event) { border-radius: 2px; } +.equipped-badge { + position: absolute; + top: 2px; + right: 2px; + font-size: 10px; + color: #fff; + background: #4caf50; + padding: 0 3px; + border-radius: 2px; + font-weight: bold; + box-shadow: 0 1px 2px rgba(0,0,0,0.3); +} + /* Item Icons (CSS Shapes) */ @@ -315,6 +333,19 @@ function updateTooltipPosition(event) { box-shadow: 0 2px 0 rgba(0,0,0,0.2); } +.icon-sunglasses::before { + content: ''; + width: 18px; + height: 8px; + background: #333; + border-radius: 2px; + display: block; + box-shadow: + inset 1px 1px 0 #555, + 4px 0 0 #000, + -4px 0 0 #000; +} + /* Floating Tooltip */ .floating-tooltip { position: fixed; /* Use fixed to position relative to viewport */ diff --git a/src/components/PetGame.vue b/src/components/PetGame.vue index 42ed27f..c5382f6 100644 --- a/src/components/PetGame.vue +++ b/src/components/PetGame.vue @@ -41,7 +41,7 @@ display: (state === 'dead' || state === 'sleep') ? 'none' : 'block', zIndex: 10 }" - :class="['state-' + state, 'stage-' + stage, { 'shaking-head': isShakingHead }]" + :class="['state-' + state, 'stage-' + stage, 'mood-' + currentMood, { 'shaking-head': isShakingHead }]" >
@@ -132,6 +132,25 @@ >
+ + +
+
+
+ + +
+
+
@@ -176,6 +195,7 @@ :hunger="stats?.hunger || 100" :happiness="stats?.happiness || 100" :health="stats?.health || 100" + :achievements="achievements" @close="showPetInfo = false" /> @@ -183,6 +203,7 @@ + + @@ -238,6 +261,14 @@ const props = defineProps({ isCleaning: { type: Boolean, default: false + }, + debugAction: { + type: Object, + default: null + }, + achievements: { + type: Array, + default: () => [] } }); @@ -259,8 +290,39 @@ const inventory = ref(new Array(16).fill(null)); inventory.value[0] = { id: 'cookie', name: '幸運餅乾', description: '增加一點快樂值', count: 5, iconClass: 'icon-cookie' }; inventory.value[1] = { id: 'water', name: '神水', description: '恢復健康', count: 2, iconClass: 'icon-water' }; inventory.value[2] = { id: 'amulet', name: '平安符', description: '保佑寵物平安', count: 1, iconClass: 'icon-amulet' }; +inventory.value[3] = { id: 'sunglasses', name: '酷酷墨鏡', description: '戴上後魅力+10', count: 1, iconClass: 'icon-sunglasses' }; const infoPage = ref(0); +// Debug & Features +const eventMessage = ref(''); +const eventAnimation = ref(null); +const equippedItems = ref([]); // Array of item IDs +const manualMood = ref(null); // For debug override +const autoMood = computed(() => { + if (props.stats.happiness < 40) return 'sad'; + if (props.stats.hunger < 30) return 'angry'; + if (props.stats.happiness > 80) return 'happy'; + return 'normal'; +}); +const currentMood = computed(() => manualMood.value || autoMood.value); + +// Watch for debug actions from parent +watch(() => props.debugAction, (action) => { + if (!action) return; + + switch (action.type) { + case 'randomEvent': + handleRandomEvent(); + break; + case 'addItem': + addItem(action.payload); + break; + case 'setMood': + setMood(action.payload); + break; + } +}); + const handlePrayerSelect = (mode) => { showPrayerMenu.value = false; @@ -339,15 +401,40 @@ function handleCloseResult() { function handleUseItem(item) { console.log('Used item:', item.name); - // TODO: Implement item effects - // Decrease count or remove item - const index = inventory.value.findIndex(i => i === item); - if (index !== -1) { - if (inventory.value[index].count > 1) { - inventory.value[index].count--; + if (item.id === 'sunglasses') { + // Toggle sunglasses + const index = equippedItems.value.indexOf('sunglasses'); + if (index === -1) { + equippedItems.value.push('sunglasses'); + showEventMessage('戴上了墨鏡!'); } else { - inventory.value[index] = null; + equippedItems.value.splice(index, 1); + showEventMessage('摘下了墨鏡。'); + } + showInventory.value = false; + return; // Don't consume the item + } + + if (item.id === 'cookie') { + emit('action', 'feed'); // Treat as feeding + showEventMessage('吃了幸運餅乾,好開心!'); + } + + if (item.id === 'water') { + emit('action', 'medicine'); // Treat as medicine + showEventMessage('喝了神水,感覺好多了!'); + } + + // Decrease count or remove item (consumables only) + if (item.id !== 'sunglasses' && item.id !== 'amulet') { + const index = inventory.value.findIndex(i => i === item); + if (index !== -1) { + if (inventory.value[index].count > 1) { + inventory.value[index].count--; + } else { + inventory.value[index] = null; + } } } @@ -358,6 +445,84 @@ function handleInventoryUpdate(newInventory) { inventory.value = newInventory; } +// --- Random Events & Debug --- + +function handleRandomEvent() { + const events = FULL_PRESETS.tinyTigerCatB.randomEvents; + const randomEvent = events[Math.floor(Math.random() * events.length)]; + + console.log(`Random Event: ${randomEvent.name}`); + + // Trigger Animation + let iconClass = ''; + let type = 'default'; + + if (randomEvent.id === 'gift_flower') { + iconClass = 'pixel-flower'; + type = 'float-up'; + } else if (randomEvent.id === 'find_treasure') { + iconClass = 'pixel-coin'; + type = 'bounce'; + } else if (randomEvent.id === 'sneeze') { + iconClass = 'pixel-wind'; + type = 'shake'; + } else if (randomEvent.id === 'play_toy') { + iconClass = 'pixel-toy'; + type = 'bounce'; + } else if (randomEvent.id === 'nightmare') { + iconClass = 'pixel-ghost'; + type = 'fade-in'; + } + + eventAnimation.value = { iconClass, type }; + + // Clear animation after 2 seconds + setTimeout(() => { + eventAnimation.value = null; + }, 2000); + + // Apply effects (simplified) + if (randomEvent.effects.happiness) { + // emit('update:stats', ...); // In a real app, we'd update stats here + } +} + +function setMood(mood) { + manualMood.value = mood; + + // Reset mood after 5 seconds + setTimeout(() => { + manualMood.value = null; + }, 5000); +} + +function addItem(itemId) { + // Find empty slot or existing item + let existingItem = inventory.value.find(i => i && i.id === itemId); + if (existingItem) { + existingItem.count++; + } else { + const emptyIndex = inventory.value.findIndex(i => i === null); + if (emptyIndex !== -1) { + if (itemId === 'sunglasses') { + inventory.value[emptyIndex] = { id: 'sunglasses', name: '酷酷墨鏡', description: '戴上後魅力+10', count: 1, iconClass: 'icon-sunglasses' }; + } else if (itemId === 'cookie') { + inventory.value[emptyIndex] = { id: 'cookie', name: '幸運餅乾', description: '增加一點快樂值', count: 1, iconClass: 'icon-cookie' }; + } + } else { + showEventMessage('背包滿了!'); + } + } +} + +function showEventMessage(msg) { + // eventMessage.value = msg; + // setTimeout(() => { + // eventMessage.value = ''; + // }, 3000); + console.log('Event:', msg); // Log to console instead +} + // Stats visibility toggle (removed local ref, using prop instead) // Poop position calculator (all on left side, strictly in game area) @@ -517,6 +682,10 @@ const foodY = ref(0); const foodStage = ref(0); // 0, 1, 2 const foodVisible = ref(false); +// Playing State +const ballX = ref(0); +const ballY = ref(0); + // Animation State const isBlinking = ref(false); @@ -531,9 +700,28 @@ const currentPixels = computed(() => { return generatePixels(CURRENT_PRESET.spriteEyesClosed); } - return isMouthOpen.value + const basePixels = isMouthOpen.value ? generatePixels(CURRENT_PRESET.spriteMouthOpen) : generatePixels(CURRENT_PRESET.sprite); + + // Apply Equipment Overlays + if (equippedItems.value.includes('sunglasses')) { + // Find sunglasses definition + const sunglasses = FULL_PRESETS.tinyTigerCatB.equipment.items.find(i => i.id === 'sunglasses_basic'); + if (sunglasses && sunglasses.overlays.child) { + sunglasses.overlays.child.pixels.forEach(p => { + // Find existing pixel at this position to overwrite, or add new + const existingIdx = basePixels.findIndex(bp => bp.x === p.x && bp.y === p.y); + if (existingIdx !== -1) { + basePixels[existingIdx].color = '#000'; // Sunglasses are black + } else { + basePixels.push({ x: p.x, y: p.y, color: '#000', className: 'accessory' }); + } + }); + } + } + + return basePixels; }); const currentFoodPixels = computed(() => { @@ -673,10 +861,15 @@ async function startFeeding() { const maxPoopRight = Math.max(...areas.map(a => a.right)); if (targetFoodX < maxPoopRight + 10) { // Move food to the right of poop areas - targetFoodX = maxPoopRight + 10; + targetFoodX = maxPoopRight + 15; } } + // Ensure food stays within bounds + const cw = containerRef.value?.clientWidth || 300; + targetFoodX = Math.max(10, Math.min(cw - 40, targetFoodX)); + + foodX.value = targetFoodX; foodX.value = targetFoodX; foodY.value = 0; // Start from top of screen @@ -690,6 +883,16 @@ async function startFeeding() { safeTargetY = Math.min(targetY, maxPoopBottom - foodSize - 5); } + // Move pet to food + const targetPetX = isFacingRight.value ? targetFoodX - width - 5 : targetFoodX + (10 * pixelSize) + 5; + + // Simple animation sequence + // 1. Drop food + // 2. Pet moves to food + // 3. Pet eats (mouth open/close) + + // ... (Existing feeding logic would go here, but we rely on state='eating' triggers from parent) + // Animate falling to front of pet const duration = 800; const startTime = performance.now(); @@ -878,7 +1081,7 @@ defineExpose({ position: relative; flex: 1; width: 100%; - overflow: hidden; + /* overflow: hidden; Removed to allow event animations to show above */ } .debug-overlay { @@ -1315,4 +1518,174 @@ defineExpose({ 0%, 100% { transform: translate(-50%, -50%) translateY(0); } 50% { transform: translate(-50%, -50%) translateY(-4px); } } + + + + +/* Mood Animations */ +.pet-root.mood-happy { + animation: bounce 0.5s infinite alternate; +} + +.pet-root.mood-angry { + filter: hue-rotate(320deg); /* Red tint */ + animation: shake 0.2s infinite; +} + +.pet-root.mood-sad { + filter: grayscale(0.5) hue-rotate(200deg); /* Blue tint */ +} + +@keyframes bounce { + from { transform: translateY(0); } + to { transform: translateY(-5px); } +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-2px); } + 75% { transform: translateX(2px); } +} + +/* Event Animations */ +.event-animation { + position: absolute; + z-index: 999; /* Ensure it's on top */ + pointer-events: none; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + /* background: rgba(255,0,0,0.2); Debug: see container */ +} + +.event-icon-anim { + /* font-size: 24px; Removed */ +} + +/* Pixel Art Icons for Events */ +/* Common base for pixels */ +[class^="pixel-"] { + width: 1px; + height: 1px; + background: transparent; + transform: scale(4); /* Make them bigger! */ + transform-origin: center; +} + +.pixel-flower { + box-shadow: + 0 -2px #ff69b4, -2px 0 #ff69b4, 2px 0 #ff69b4, 0 2px #ff69b4, /* Petals */ + -1px -1px #ff1493, 1px -1px #ff1493, -1px 1px #ff1493, 1px 1px #ff1493, /* Inner Petals */ + 0 0 #ffff00; /* Center */ + background: #ffff00; /* Ensure center is visible */ +} + +.pixel-coin { + box-shadow: + -1px -2px #ffd700, 0px -2px #ffd700, 1px -2px #ffd700, + -2px -1px #ffd700, 2px -1px #ffd700, + -2px 0px #ffd700, 0px 0px #daa520, 2px 0px #ffd700, + -2px 1px #ffd700, 2px 1px #ffd700, + -1px 2px #ffd700, 0px 2px #ffd700, 1px 2px #ffd700; + background: #daa520; +} + +.pixel-wind { + box-shadow: + -2px -2px #fff, -1px -2px #fff, + 0px -1px #fff, 1px -1px #fff, 2px -1px #fff, + -3px 0px #fff, -2px 0px #fff, + -1px 1px #fff, 0px 1px #fff; + opacity: 0.8; + background: transparent; +} + +.pixel-toy { + box-shadow: + -1px -2px #32cd32, 0px -2px #32cd32, 1px -2px #32cd32, + -2px -1px #32cd32, 2px -1px #32cd32, + -2px 0px #32cd32, 2px 0px #32cd32, + -2px 1px #32cd32, 2px 1px #32cd32, + -1px 2px #32cd32, 0px 2px #32cd32, 1px 2px #32cd32, + /* Stripe */ + -2px 0px #fff, -1px 0px #fff, 0px 0px #fff, 1px 0px #fff, 2px 0px #fff; + background: #32cd32; +} + +.pixel-ghost { + box-shadow: + -1px -3px #fff, 0px -3px #fff, 1px -3px #fff, + -2px -2px #fff, 2px -2px #fff, + -2px -1px #fff, -1px -1px #000, 1px -1px #000, 2px -1px #fff, /* Eyes */ + -2px 0px #fff, 2px 0px #fff, + -2px 1px #fff, 2px 1px #fff, + -2px 2px #fff, -1px 2px #fff, 0px 2px #fff, 1px 2px #fff, 2px 2px #fff, + -2px 3px #fff, 0px 3px #fff, 2px 3px #fff; + background: #fff; +} + +.event-animation.float-up { + animation: floatUp 2s ease-out forwards; +} + +.event-animation.bounce { + animation: bounceAnim 1s ease-in-out infinite; +} + +.event-animation.shake { + animation: shakeAnim 0.5s ease-in-out infinite; +} + +.event-animation.fade-in { + animation: fadeInOut 2s ease-in-out forwards; +} + +@keyframes floatUp { + 0% { transform: translateY(0) scale(0.5); opacity: 0; } + 20% { transform: translateY(-10px) scale(1.2); opacity: 1; } + 80% { transform: translateY(-30px) scale(1); opacity: 1; } + 100% { transform: translateY(-40px) scale(0.8); opacity: 0; } +} + +@keyframes bounceAnim { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-15px); } +} + +@keyframes shakeAnim { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px) rotate(-10deg); } + 75% { transform: translateX(5px) rotate(10deg); } +} + +@keyframes fadeInOut { + 0% { opacity: 0; transform: scale(0.5); } + 20% { opacity: 1; transform: scale(1.2); } + 80% { opacity: 1; transform: scale(1); } + 100% { opacity: 0; transform: scale(0.5); } +} + +/* Play Ball */ +.play-ball { + position: absolute; + width: 16px; + height: 16px; + z-index: 15; + animation: ballBounce 1s infinite; +} + +.ball-pixel { + width: 100%; + height: 100%; + background: #ff4081; + border-radius: 50%; + box-shadow: inset -2px -2px 0 rgba(0,0,0,0.2); +} + +@keyframes ballBounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-40px); } +} diff --git a/src/components/PetInfoScreen.vue b/src/components/PetInfoScreen.vue index 1b271b2..47e0d90 100644 --- a/src/components/PetInfoScreen.vue +++ b/src/components/PetInfoScreen.vue @@ -1,8 +1,30 @@