good version
This commit is contained in:
parent
01cf45ceb9
commit
3c9a7f1e7b
30
src/App.vue
30
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() };
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -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) {
|
|||
<button v-if="stage === 'egg'" @click="hatchEgg()">🥚 Hatch Egg</button>
|
||||
<button v-else @click="reset()">🔄 Reset to Egg</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button @click="triggerDebugAction('randomEvent')">🎲 Random Event</button>
|
||||
<button @click="triggerDebugAction('addItem', 'sunglasses')">🕶️ Add Sunglasses</button>
|
||||
<button @click="triggerDebugAction('addItem', 'cookie')">🍪 Add Cookie</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button @click="triggerDebugAction('setMood', 'happy')">😊 Happy</button>
|
||||
<button @click="triggerDebugAction('setMood', 'angry')">😠 Angry</button>
|
||||
<button @click="triggerDebugAction('setMood', 'sad')">😢 Sad</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button @click="unlockAllAchievements()">🏆 Unlock All Achievements</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -140,4 +140,6 @@ defineEmits(['clean', 'medicine', 'training', 'inventory']);
|
|||
/* Bottom */
|
||||
-2px 6px 0 #8d6e63, 0px 6px 0 #8d6e63, 2px 6px 0 #8d6e63;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<template v-if="item">
|
||||
<div class="item-icon" :class="item.iconClass"></div>
|
||||
<div class="item-count" v-if="item.count > 1">x{{ item.count }}</div>
|
||||
<div class="equipped-badge" v-if="isEquipped(item)">E</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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 }]"
|
||||
>
|
||||
<div class="pet-inner" :class="isFacingRight ? 'face-right' : 'face-left'">
|
||||
<!-- 根據是否張嘴選擇顯示的像素 -->
|
||||
|
|
@ -132,6 +132,25 @@
|
|||
>
|
||||
<div class="wave-drop"></div>
|
||||
</div>
|
||||
|
||||
<!-- Play Ball Animation -->
|
||||
<div
|
||||
v-if="state === 'playing'"
|
||||
class="play-ball"
|
||||
:style="{ left: ballX + 'px', top: ballY + 'px' }"
|
||||
>
|
||||
<div class="ball-pixel"></div>
|
||||
</div>
|
||||
|
||||
<!-- Random Event Animation -->
|
||||
<div
|
||||
v-if="eventAnimation"
|
||||
class="event-animation"
|
||||
:class="eventAnimation.type"
|
||||
:style="{ left: (petX + width/2 - 16) + 'px', top: (petY - 40) + 'px' }"
|
||||
>
|
||||
<div :class="eventAnimation.iconClass"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prayer Menu (覆蓋整個遊戲區域) -->
|
||||
|
|
@ -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 @@
|
|||
<InventoryScreen
|
||||
v-if="showInventory"
|
||||
:inventory="inventory"
|
||||
:equippedItems="equippedItems"
|
||||
@close="showInventory = false"
|
||||
@use-item="handleUseItem"
|
||||
@update:inventory="handleInventoryUpdate"
|
||||
|
|
@ -199,6 +220,8 @@
|
|||
@training="showPrayerMenu = true"
|
||||
@inventory="showInventory = !showInventory"
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
|
@ -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,9 +401,33 @@ function handleCloseResult() {
|
|||
|
||||
function handleUseItem(item) {
|
||||
console.log('Used item:', item.name);
|
||||
// TODO: Implement item effects
|
||||
|
||||
// Decrease count or remove item
|
||||
if (item.id === 'sunglasses') {
|
||||
// Toggle sunglasses
|
||||
const index = equippedItems.value.indexOf('sunglasses');
|
||||
if (index === -1) {
|
||||
equippedItems.value.push('sunglasses');
|
||||
showEventMessage('戴上了墨鏡!');
|
||||
} else {
|
||||
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) {
|
||||
|
|
@ -350,6 +436,7 @@ function handleUseItem(item) {
|
|||
inventory.value[index] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showInventory.value = false;
|
||||
}
|
||||
|
|
@ -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); }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,28 @@
|
|||
<template>
|
||||
<div class="pet-info-screen" @click="$emit('close')">
|
||||
<div class="info-container">
|
||||
<!-- Tabs -->
|
||||
<div class="info-tabs" @click.stop>
|
||||
<button
|
||||
class="tab-btn"
|
||||
:class="{ active: activeTab === 0 }"
|
||||
@click="activeTab = 0"
|
||||
>
|
||||
狀態
|
||||
</button>
|
||||
<button
|
||||
class="tab-btn"
|
||||
:class="{ active: activeTab === 1 }"
|
||||
@click="activeTab = 1"
|
||||
>
|
||||
成就
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Close Button Removed -->
|
||||
|
||||
<!-- Stats View -->
|
||||
<div v-if="activeTab === 0" class="stats-view">
|
||||
<!-- Stats Bars at Top (Pixel Style) -->
|
||||
<div class="stats-section">
|
||||
<div class="stat-item">
|
||||
|
|
@ -102,11 +124,34 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Achievements View -->
|
||||
<div v-if="activeTab === 1" class="achievements-view">
|
||||
<div class="info-title">═ 成就列表 ═</div>
|
||||
<div class="achievements-list">
|
||||
<div
|
||||
v-for="ach in achievements"
|
||||
:key="ach.id"
|
||||
class="achievement-item"
|
||||
:class="{ unlocked: ach.unlocked }"
|
||||
>
|
||||
<div class="ach-icon">
|
||||
<div v-if="ach.unlocked" :class="'pixel-icon-' + ach.id"></div>
|
||||
<div v-else class="pixel-icon-locked"></div>
|
||||
</div>
|
||||
<div class="ach-info">
|
||||
<div class="ach-name">{{ ach.name }}</div>
|
||||
<div class="ach-desc">{{ ach.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
petName: String,
|
||||
|
|
@ -124,9 +169,15 @@ const props = defineProps({
|
|||
health: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
achievements: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const activeTab = ref(0);
|
||||
|
||||
defineEmits(['close']);
|
||||
|
||||
// Display values (ceiling for bars)
|
||||
|
|
@ -180,6 +231,76 @@ const weight = computed(() => {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: min-content;
|
||||
position: relative; /* For absolute positioning of close button */
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #d32f2f;
|
||||
border: 2px solid #8b4513;
|
||||
color: white;
|
||||
font-family: 'DotGothic16', monospace;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 0 #8b4513;
|
||||
}
|
||||
|
||||
.close-btn:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.info-tabs {
|
||||
display: flex;
|
||||
background: #c49454;
|
||||
padding: 8px 6px 0 6px; /* Increased top padding */
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
border: 2px solid transparent; /* Reserve space */
|
||||
border-bottom: none;
|
||||
background: #a67c43;
|
||||
color: #3d2f1f;
|
||||
font-family: 'DotGothic16', monospace;
|
||||
font-size: 12px;
|
||||
padding: 4px 0;
|
||||
text-align: center;
|
||||
border-radius: 4px 4px 0 0;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
box-sizing: border-box;
|
||||
height: 32px;
|
||||
outline: none; /* Remove blue focus ring */
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: #f0d09c; /* Match body bg */
|
||||
font-weight: bold;
|
||||
top: 0;
|
||||
border-color: #8b6f47;
|
||||
border-bottom: 2px solid #f0d09c; /* Blend with content */
|
||||
z-index: 2;
|
||||
height: 34px; /* Slightly taller to cover bottom gap */
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.stats-view, .achievements-view {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Stats Section - Pixel Style */
|
||||
|
|
@ -404,4 +525,180 @@ const weight = computed(() => {
|
|||
color: #3d2f1f;
|
||||
font-weight: 600;
|
||||
}
|
||||
.value {
|
||||
font-family: 'DotGothic16', monospace;
|
||||
font-size: 11px;
|
||||
color: #3d2f1f;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Achievements */
|
||||
.achievements-view {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.achievements-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.achievement-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
background: #dcb880;
|
||||
border: 2px solid #c49454;
|
||||
border-radius: 4px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.achievement-item.unlocked {
|
||||
background: #fff8e0;
|
||||
border-color: #ffd700;
|
||||
opacity: 1;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.ach-icon {
|
||||
font-size: 20px;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ach-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ach-name {
|
||||
font-family: 'DotGothic16', monospace;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
color: #3d2f1f;
|
||||
}
|
||||
|
||||
.ach-desc {
|
||||
font-family: 'DotGothic16', monospace;
|
||||
font-size: 10px;
|
||||
color: #665544;
|
||||
}
|
||||
/* Pixel Icons for Achievements */
|
||||
.ach-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Common Pixel Art Base */
|
||||
.pixel-art {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
background: transparent;
|
||||
transform: scale(2); /* Scale up the 1px pixels */
|
||||
}
|
||||
|
||||
/* Locked Icon (Padlock) */
|
||||
.pixel-icon-locked {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
background: transparent;
|
||||
transform: scale(2);
|
||||
color: #8b6f47;
|
||||
box-shadow:
|
||||
/* Body */
|
||||
-3px 0, -2px 0, -1px 0, 0px 0, 1px 0, 2px 0, 3px 0,
|
||||
-3px 1px, -2px 1px, -1px 1px, 0px 1px, 1px 1px, 2px 1px, 3px 1px,
|
||||
-3px 2px, -2px 2px, -1px 2px, 0px 2px, 1px 2px, 2px 2px, 3px 2px,
|
||||
-3px 3px, -2px 3px, -1px 3px, 0px 3px, 1px 3px, 2px 3px, 3px 3px,
|
||||
/* Shackle */
|
||||
-2px -1px, 2px -1px,
|
||||
-2px -2px, 2px -2px,
|
||||
-1px -3px, 0px -3px, 1px -3px;
|
||||
}
|
||||
|
||||
/* Newbie (Chick) - Yellow */
|
||||
.pixel-icon-newbie {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
background: transparent;
|
||||
transform: scale(2);
|
||||
box-shadow:
|
||||
/* Top feathers */
|
||||
0px -5px #ffe135,
|
||||
/* Head */
|
||||
-2px -4px #ffe135, -1px -4px #ffe135, 0px -4px #ffe135, 1px -4px #ffe135, 2px -4px #ffe135,
|
||||
-3px -3px #ffe135, -2px -3px #ffe135, -1px -3px #ffe135, 0px -3px #ffe135, 1px -3px #ffe135, 2px -3px #ffe135, 3px -3px #ffe135,
|
||||
-3px -2px #ffe135, -2px -2px #000, -1px -2px #ffe135, 0px -2px #ffe135, 1px -2px #ffe135, 2px -2px #000, 3px -2px #ffe135, /* Eyes */
|
||||
-3px -1px #ffe135, -2px -1px #ff69b4, -1px -1px #ffe135, 0px -1px #ff8c00, 1px -1px #ffe135, 2px -1px #ff69b4, 3px -1px #ffe135, /* Cheeks & Beak */
|
||||
/* Body */
|
||||
-3px 0px #ffe135, -2px 0px #ffe135, -1px 0px #ffe135, 0px 0px #ffe135, 1px 0px #ffe135, 2px 0px #ffe135, 3px 0px #ffe135,
|
||||
-2px 1px #ffe135, -1px 1px #ffe135, 0px 1px #ffe135, 1px 1px #ffe135, 2px 1px #ffe135,
|
||||
/* Feet */
|
||||
-2px 2px #ff8c00, 2px 2px #ff8c00;
|
||||
}
|
||||
|
||||
/* Veteran (Mario Style) */
|
||||
.pixel-icon-veteran {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
background: transparent;
|
||||
transform: scale(2);
|
||||
box-shadow:
|
||||
/* Hat */
|
||||
-2px -6px #f00, -1px -6px #f00, 0px -6px #f00, 1px -6px #f00, 2px -6px #f00,
|
||||
-3px -5px #f00, -2px -5px #f00, -1px -5px #f00, 0px -5px #f00, 1px -5px #f00, 2px -5px #f00, 3px -5px #f00,
|
||||
/* Face */
|
||||
-2px -4px #fc9, -1px -4px #fc9, 0px -4px #fc9, 1px -4px #fc9, 2px -4px #000, 3px -4px #fc9,
|
||||
-3px -3px #fc9, -2px -3px #fc9, -1px -3px #fc9, 0px -3px #fc9, 1px -3px #000, 2px -3px #fc9, 3px -3px #fc9,
|
||||
/* Mustache */
|
||||
-1px -2px #000, 0px -2px #000, 1px -2px #000, 2px -2px #000,
|
||||
/* Body */
|
||||
-2px -1px #f00, -1px -1px #00f, 0px -1px #f00, 1px -1px #00f, 2px -1px #f00,
|
||||
-2px 0px #f00, -1px 0px #00f, 0px 0px #00f, 1px 0px #00f, 2px 0px #f00,
|
||||
-2px 1px #00f, -1px 1px #00f, 0px 1px #00f, 1px 1px #00f, 2px 1px #00f,
|
||||
/* Shoes */
|
||||
-2px 2px #8b4513, -1px 2px #8b4513, 1px 2px #8b4513, 2px 2px #8b4513;
|
||||
}
|
||||
|
||||
/* Healthy (Donald Style) */
|
||||
.pixel-icon-healthy {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
background: transparent;
|
||||
transform: scale(2);
|
||||
box-shadow:
|
||||
/* Hat */
|
||||
-2px -6px #1e90ff, -1px -6px #1e90ff, 0px -6px #1e90ff, 1px -6px #1e90ff,
|
||||
-3px -5px #000, -2px -5px #1e90ff, -1px -5px #1e90ff, 0px -5px #1e90ff, 1px -5px #1e90ff, 2px -5px #000,
|
||||
/* Face */
|
||||
-2px -4px #fff, -1px -4px #fff, 0px -4px #fff, 1px -4px #fff,
|
||||
-3px -3px #fff, -2px -3px #00f, -1px -3px #fff, 0px -3px #fff, 1px -3px #00f, 2px -3px #fff, /* Eyes */
|
||||
/* Beak */
|
||||
-2px -2px #ffa500, -1px -2px #ffa500, 0px -2px #ffa500, 1px -2px #ffa500,
|
||||
-1px -1px #ffa500, 0px -1px #ffa500,
|
||||
/* Body (Sailor) */
|
||||
-2px 0px #1e90ff, -1px 0px #1e90ff, 0px 0px #d00, 1px 0px #1e90ff, 2px 0px #1e90ff, /* Bowtie */
|
||||
-2px 1px #fff, -1px 1px #fff, 0px 1px #fff, 1px 1px #fff, 2px 1px #fff;
|
||||
}
|
||||
|
||||
/* Happy (Heart) */
|
||||
.pixel-icon-happy {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
background: transparent;
|
||||
transform: scale(2);
|
||||
color: #ff1493;
|
||||
box-shadow:
|
||||
-2px -2px, -1px -2px, 1px -2px, 2px -2px,
|
||||
-3px -1px, -2px -1px, -1px -1px, 0px -1px, 1px -1px, 2px -1px, 3px -1px,
|
||||
-3px 0px, -2px 0px, -1px 0px, 0px 0px, 1px 0px, 2px 0px, 3px 0px,
|
||||
-2px 1px, -1px 1px, 0px 1px, 1px 1px, 2px 1px,
|
||||
-1px 2px, 0px 2px, 1px 2px,
|
||||
0px 3px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -11,13 +11,22 @@ export function usePetSystem() {
|
|||
happiness: 100, // 0-100 (0 = Depressed)
|
||||
health: 100, // 0-100 (0 = Sick risk)
|
||||
weight: 500, // grams
|
||||
age: 0, // days
|
||||
age: 1, // days (start at day 1)
|
||||
poopCount: 0 // Number of poops on screen
|
||||
});
|
||||
|
||||
const achievements = ref([
|
||||
{ id: 'newbie', name: '新手飼主', desc: '養育超過 1 天', unlocked: false, icon: '🥚' },
|
||||
{ id: 'veteran', name: '資深飼主', desc: '養育超過 7 天', unlocked: false, icon: '🏆' },
|
||||
{ id: 'healthy', name: '健康寶寶', desc: '3歲且健康 > 90', unlocked: false, icon: '💪' },
|
||||
{ id: 'happy', name: '快樂天使', desc: '3歲且快樂 > 90', unlocked: false, icon: '💖' }
|
||||
]);
|
||||
|
||||
// --- Internal Timers ---
|
||||
let gameLoopId = null;
|
||||
let tickCount = 0;
|
||||
const TICK_RATE = 3000; // 3 seconds per tick
|
||||
const TICKS_PER_DAY = 20; // For testing: 1 minute = 1 day (usually 28800 for 24h)
|
||||
|
||||
const isCleaning = ref(false);
|
||||
|
||||
|
|
@ -145,9 +154,53 @@ export function usePetSystem() {
|
|||
// state.value = 'dead';
|
||||
// }
|
||||
|
||||
// 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.
|
||||
// Evolution / Growth
|
||||
tickCount++;
|
||||
if (tickCount >= TICKS_PER_DAY) {
|
||||
stats.value.age++;
|
||||
tickCount = 0;
|
||||
checkEvolution();
|
||||
}
|
||||
|
||||
checkAchievements();
|
||||
}
|
||||
|
||||
function checkAchievements() {
|
||||
if (!achievements.value[0].unlocked && stats.value.age >= 1) {
|
||||
unlockAchievement(0);
|
||||
}
|
||||
if (!achievements.value[1].unlocked && stats.value.age >= 7) {
|
||||
unlockAchievement(1);
|
||||
}
|
||||
if (!achievements.value[2].unlocked && stats.value.age >= 3 && stats.value.health >= 90) {
|
||||
unlockAchievement(2);
|
||||
}
|
||||
if (!achievements.value[3].unlocked && stats.value.age >= 3 && stats.value.happiness >= 90) {
|
||||
unlockAchievement(3);
|
||||
}
|
||||
}
|
||||
|
||||
function unlockAchievement(index) {
|
||||
if (!achievements.value[index].unlocked) {
|
||||
achievements.value[index].unlocked = true;
|
||||
triggerState('happy', 2000); // Celebrate achievement
|
||||
}
|
||||
}
|
||||
|
||||
function unlockAllAchievements() {
|
||||
achievements.value.forEach(a => a.unlocked = true);
|
||||
triggerState('happy', 2000);
|
||||
}
|
||||
|
||||
function checkEvolution() {
|
||||
// Simple evolution logic
|
||||
if (stage.value === 'baby' && stats.value.age >= 3) {
|
||||
stage.value = 'child';
|
||||
triggerState('happy', 2000); // Celebrate
|
||||
} else if (stage.value === 'child' && stats.value.age >= 7) {
|
||||
stage.value = 'adult';
|
||||
triggerState('happy', 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
|
@ -184,9 +237,10 @@ export function usePetSystem() {
|
|||
happiness: 100,
|
||||
health: 100,
|
||||
weight: 500,
|
||||
age: 0,
|
||||
age: 1,
|
||||
poopCount: 0
|
||||
};
|
||||
tickCount = 0;
|
||||
}
|
||||
|
||||
// --- Lifecycle ---
|
||||
|
|
@ -208,6 +262,8 @@ export function usePetSystem() {
|
|||
clean,
|
||||
sleep,
|
||||
hatchEgg,
|
||||
reset
|
||||
reset,
|
||||
achievements,
|
||||
unlockAllAchievements
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue