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() };
+}
@@ -95,6 +106,8 @@ function setPetState(newState) {
:stats="stats"
:isCleaning="isCleaning"
:showStats="showStats"
+ :debugAction="debugAction"
+ :achievements="achievements"
@update:state="state = $event"
@action="handleAction"
/>
@@ -120,6 +133,19 @@ function setPetState(newState) {
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
x{{ item.count }}
+ E
@@ -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 }]"
>
+
+
+
+
+
+
@@ -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 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
飢餓
@@ -101,12 +123,35 @@
{{ baseStats.speed }}
+
+
+
+
+
═ 成就列表 ═
+
+
+
+
+
{{ ach.name }}
+
{{ ach.desc }}
+
+
+
+