good version

This commit is contained in:
王性驊 2025-11-22 11:23:02 +08:00
parent 01cf45ceb9
commit 3c9a7f1e7b
6 changed files with 812 additions and 27 deletions

View File

@ -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>

View File

@ -140,4 +140,6 @@ defineEmits(['clean', 'medicine', 'training', 'inventory']);
/* Bottom */
-2px 6px 0 #8d6e63, 0px 6px 0 #8d6e63, 2px 6px 0 #8d6e63;
}
</style>

View File

@ -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 */

View File

@ -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>

View File

@ -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>

View File

@ -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
};
}