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 currentScreen = ref('game');
|
||||||
const petGameRef = ref(null);
|
const petGameRef = ref(null);
|
||||||
const showStats = ref(false); // Stats visibility
|
const showStats = ref(false); // Stats visibility
|
||||||
|
const debugAction = ref(null); // For passing debug commands to PetGame
|
||||||
|
|
||||||
// Initialize Pet System
|
// Initialize Pet System
|
||||||
const {
|
const {
|
||||||
|
|
@ -21,7 +22,9 @@ const {
|
||||||
clean,
|
clean,
|
||||||
isCleaning,
|
isCleaning,
|
||||||
hatchEgg,
|
hatchEgg,
|
||||||
reset
|
reset,
|
||||||
|
achievements,
|
||||||
|
unlockAllAchievements
|
||||||
} = usePetSystem();
|
} = usePetSystem();
|
||||||
|
|
||||||
// Handle Action Menu Events
|
// Handle Action Menu Events
|
||||||
|
|
@ -38,7 +41,11 @@ function handleAction(action) {
|
||||||
clean();
|
clean();
|
||||||
break;
|
break;
|
||||||
case 'play':
|
case 'play':
|
||||||
play();
|
if (play()) {
|
||||||
|
if (petGameRef.value) {
|
||||||
|
petGameRef.value.startPlaying();
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'sleep':
|
case 'sleep':
|
||||||
sleep();
|
sleep();
|
||||||
|
|
@ -81,6 +88,10 @@ function handleAction(action) {
|
||||||
function setPetState(newState) {
|
function setPetState(newState) {
|
||||||
state.value = newState;
|
state.value = newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function triggerDebugAction(action, payload = null) {
|
||||||
|
debugAction.value = { type: action, payload, timestamp: Date.now() };
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -95,6 +106,8 @@ function setPetState(newState) {
|
||||||
:stats="stats"
|
:stats="stats"
|
||||||
:isCleaning="isCleaning"
|
:isCleaning="isCleaning"
|
||||||
:showStats="showStats"
|
:showStats="showStats"
|
||||||
|
:debugAction="debugAction"
|
||||||
|
:achievements="achievements"
|
||||||
@update:state="state = $event"
|
@update:state="state = $event"
|
||||||
@action="handleAction"
|
@action="handleAction"
|
||||||
/>
|
/>
|
||||||
|
|
@ -120,6 +133,19 @@ function setPetState(newState) {
|
||||||
<button v-if="stage === 'egg'" @click="hatchEgg()">🥚 Hatch Egg</button>
|
<button v-if="stage === 'egg'" @click="hatchEgg()">🥚 Hatch Egg</button>
|
||||||
<button v-else @click="reset()">🔄 Reset to Egg</button>
|
<button v-else @click="reset()">🔄 Reset to Egg</button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -140,4 +140,6 @@ defineEmits(['clean', 'medicine', 'training', 'inventory']);
|
||||||
/* Bottom */
|
/* Bottom */
|
||||||
-2px 6px 0 #8d6e63, 0px 6px 0 #8d6e63, 2px 6px 0 #8d6e63;
|
-2px 6px 0 #8d6e63, 0px 6px 0 #8d6e63, 2px 6px 0 #8d6e63;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
<template v-if="item">
|
<template v-if="item">
|
||||||
<div class="item-icon" :class="item.iconClass"></div>
|
<div class="item-icon" :class="item.iconClass"></div>
|
||||||
<div class="item-count" v-if="item.count > 1">x{{ item.count }}</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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -54,11 +55,11 @@ import { ref, computed } from 'vue';
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
inventory: {
|
inventory: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [
|
default: () => []
|
||||||
{ id: 'cookie', name: '幸運餅乾', description: '增加一點快樂值', count: 5, iconClass: 'icon-cookie' },
|
},
|
||||||
{ id: 'water', name: '神水', description: '恢復健康', count: 2, iconClass: 'icon-water' },
|
equippedItems: {
|
||||||
{ id: 'amulet', name: '平安符', description: '保佑寵物平安', count: 1, iconClass: 'icon-amulet' }
|
type: Array,
|
||||||
]
|
default: () => []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -131,6 +132,10 @@ function useItem() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isEquipped(item) {
|
||||||
|
return props.equippedItems.includes(item.id);
|
||||||
|
}
|
||||||
|
|
||||||
// Tooltip Logic
|
// Tooltip Logic
|
||||||
const hoveredItem = ref(null);
|
const hoveredItem = ref(null);
|
||||||
const tooltipStyle = ref({ top: '0px', left: '0px' });
|
const tooltipStyle = ref({ top: '0px', left: '0px' });
|
||||||
|
|
@ -282,6 +287,19 @@ function updateTooltipPosition(event) {
|
||||||
border-radius: 2px;
|
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) */
|
/* Item Icons (CSS Shapes) */
|
||||||
|
|
@ -315,6 +333,19 @@ function updateTooltipPosition(event) {
|
||||||
box-shadow: 0 2px 0 rgba(0,0,0,0.2);
|
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 */
|
||||||
.floating-tooltip {
|
.floating-tooltip {
|
||||||
position: fixed; /* Use fixed to position relative to viewport */
|
position: fixed; /* Use fixed to position relative to viewport */
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
display: (state === 'dead' || state === 'sleep') ? 'none' : 'block',
|
display: (state === 'dead' || state === 'sleep') ? 'none' : 'block',
|
||||||
zIndex: 10
|
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'">
|
<div class="pet-inner" :class="isFacingRight ? 'face-right' : 'face-left'">
|
||||||
<!-- 根據是否張嘴選擇顯示的像素 -->
|
<!-- 根據是否張嘴選擇顯示的像素 -->
|
||||||
|
|
@ -132,6 +132,25 @@
|
||||||
>
|
>
|
||||||
<div class="wave-drop"></div>
|
<div class="wave-drop"></div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Prayer Menu (覆蓋整個遊戲區域) -->
|
<!-- Prayer Menu (覆蓋整個遊戲區域) -->
|
||||||
|
|
@ -176,6 +195,7 @@
|
||||||
:hunger="stats?.hunger || 100"
|
:hunger="stats?.hunger || 100"
|
||||||
:happiness="stats?.happiness || 100"
|
:happiness="stats?.happiness || 100"
|
||||||
:health="stats?.health || 100"
|
:health="stats?.health || 100"
|
||||||
|
:achievements="achievements"
|
||||||
@close="showPetInfo = false"
|
@close="showPetInfo = false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -183,6 +203,7 @@
|
||||||
<InventoryScreen
|
<InventoryScreen
|
||||||
v-if="showInventory"
|
v-if="showInventory"
|
||||||
:inventory="inventory"
|
:inventory="inventory"
|
||||||
|
:equippedItems="equippedItems"
|
||||||
@close="showInventory = false"
|
@close="showInventory = false"
|
||||||
@use-item="handleUseItem"
|
@use-item="handleUseItem"
|
||||||
@update:inventory="handleInventoryUpdate"
|
@update:inventory="handleInventoryUpdate"
|
||||||
|
|
@ -199,6 +220,8 @@
|
||||||
@training="showPrayerMenu = true"
|
@training="showPrayerMenu = true"
|
||||||
@inventory="showInventory = !showInventory"
|
@inventory="showInventory = !showInventory"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -238,6 +261,14 @@ const props = defineProps({
|
||||||
isCleaning: {
|
isCleaning: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
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[0] = { id: 'cookie', name: '幸運餅乾', description: '增加一點快樂值', count: 5, iconClass: 'icon-cookie' };
|
||||||
inventory.value[1] = { id: 'water', name: '神水', description: '恢復健康', count: 2, iconClass: 'icon-water' };
|
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[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);
|
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) => {
|
const handlePrayerSelect = (mode) => {
|
||||||
showPrayerMenu.value = false;
|
showPrayerMenu.value = false;
|
||||||
|
|
||||||
|
|
@ -339,9 +401,33 @@ function handleCloseResult() {
|
||||||
|
|
||||||
function handleUseItem(item) {
|
function handleUseItem(item) {
|
||||||
console.log('Used item:', item.name);
|
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);
|
const index = inventory.value.findIndex(i => i === item);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
if (inventory.value[index].count > 1) {
|
if (inventory.value[index].count > 1) {
|
||||||
|
|
@ -350,6 +436,7 @@ function handleUseItem(item) {
|
||||||
inventory.value[index] = null;
|
inventory.value[index] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
showInventory.value = false;
|
showInventory.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -358,6 +445,84 @@ function handleInventoryUpdate(newInventory) {
|
||||||
inventory.value = 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)
|
// Stats visibility toggle (removed local ref, using prop instead)
|
||||||
|
|
||||||
// Poop position calculator (all on left side, strictly in game area)
|
// 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 foodStage = ref(0); // 0, 1, 2
|
||||||
const foodVisible = ref(false);
|
const foodVisible = ref(false);
|
||||||
|
|
||||||
|
// Playing State
|
||||||
|
const ballX = ref(0);
|
||||||
|
const ballY = ref(0);
|
||||||
|
|
||||||
// Animation State
|
// Animation State
|
||||||
const isBlinking = ref(false);
|
const isBlinking = ref(false);
|
||||||
|
|
||||||
|
|
@ -531,9 +700,28 @@ const currentPixels = computed(() => {
|
||||||
return generatePixels(CURRENT_PRESET.spriteEyesClosed);
|
return generatePixels(CURRENT_PRESET.spriteEyesClosed);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isMouthOpen.value
|
const basePixels = isMouthOpen.value
|
||||||
? generatePixels(CURRENT_PRESET.spriteMouthOpen)
|
? generatePixels(CURRENT_PRESET.spriteMouthOpen)
|
||||||
: generatePixels(CURRENT_PRESET.sprite);
|
: 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(() => {
|
const currentFoodPixels = computed(() => {
|
||||||
|
|
@ -673,10 +861,15 @@ async function startFeeding() {
|
||||||
const maxPoopRight = Math.max(...areas.map(a => a.right));
|
const maxPoopRight = Math.max(...areas.map(a => a.right));
|
||||||
if (targetFoodX < maxPoopRight + 10) {
|
if (targetFoodX < maxPoopRight + 10) {
|
||||||
// Move food to the right of poop areas
|
// 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;
|
foodX.value = targetFoodX;
|
||||||
foodY.value = 0; // Start from top of screen
|
foodY.value = 0; // Start from top of screen
|
||||||
|
|
||||||
|
|
@ -690,6 +883,16 @@ async function startFeeding() {
|
||||||
safeTargetY = Math.min(targetY, maxPoopBottom - foodSize - 5);
|
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
|
// Animate falling to front of pet
|
||||||
const duration = 800;
|
const duration = 800;
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
@ -878,7 +1081,7 @@ defineExpose({
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
/* overflow: hidden; Removed to allow event animations to show above */
|
||||||
}
|
}
|
||||||
|
|
||||||
.debug-overlay {
|
.debug-overlay {
|
||||||
|
|
@ -1315,4 +1518,174 @@ defineExpose({
|
||||||
0%, 100% { transform: translate(-50%, -50%) translateY(0); }
|
0%, 100% { transform: translate(-50%, -50%) translateY(0); }
|
||||||
50% { transform: translate(-50%, -50%) translateY(-4px); }
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,28 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="pet-info-screen" @click="$emit('close')">
|
<div class="pet-info-screen" @click="$emit('close')">
|
||||||
<div class="info-container">
|
<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) -->
|
<!-- Stats Bars at Top (Pixel Style) -->
|
||||||
<div class="stats-section">
|
<div class="stats-section">
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
|
|
@ -102,11 +124,34 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
petName: String,
|
petName: String,
|
||||||
|
|
@ -124,9 +169,15 @@ const props = defineProps({
|
||||||
health: {
|
health: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 100
|
default: 100
|
||||||
|
},
|
||||||
|
achievements: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const activeTab = ref(0);
|
||||||
|
|
||||||
defineEmits(['close']);
|
defineEmits(['close']);
|
||||||
|
|
||||||
// Display values (ceiling for bars)
|
// Display values (ceiling for bars)
|
||||||
|
|
@ -180,6 +231,76 @@ const weight = computed(() => {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: min-content;
|
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 */
|
/* Stats Section - Pixel Style */
|
||||||
|
|
@ -404,4 +525,180 @@ const weight = computed(() => {
|
||||||
color: #3d2f1f;
|
color: #3d2f1f;
|
||||||
font-weight: 600;
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,22 @@ export function usePetSystem() {
|
||||||
happiness: 100, // 0-100 (0 = Depressed)
|
happiness: 100, // 0-100 (0 = Depressed)
|
||||||
health: 100, // 0-100 (0 = Sick risk)
|
health: 100, // 0-100 (0 = Sick risk)
|
||||||
weight: 500, // grams
|
weight: 500, // grams
|
||||||
age: 0, // days
|
age: 1, // days (start at day 1)
|
||||||
poopCount: 0 // Number of poops on screen
|
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 ---
|
// --- Internal Timers ---
|
||||||
let gameLoopId = null;
|
let gameLoopId = null;
|
||||||
|
let tickCount = 0;
|
||||||
const TICK_RATE = 3000; // 3 seconds per tick
|
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);
|
const isCleaning = ref(false);
|
||||||
|
|
||||||
|
|
@ -145,9 +154,53 @@ export function usePetSystem() {
|
||||||
// state.value = 'dead';
|
// state.value = 'dead';
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// Evolution / Growth (Simple Age increment)
|
// Evolution / Growth
|
||||||
// In a real game, 1 day might be 24h, here maybe every 100 ticks?
|
tickCount++;
|
||||||
// For now, let's just say age increases slowly.
|
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 ---
|
// --- Helpers ---
|
||||||
|
|
@ -184,9 +237,10 @@ export function usePetSystem() {
|
||||||
happiness: 100,
|
happiness: 100,
|
||||||
health: 100,
|
health: 100,
|
||||||
weight: 500,
|
weight: 500,
|
||||||
age: 0,
|
age: 1,
|
||||||
poopCount: 0
|
poopCount: 0
|
||||||
};
|
};
|
||||||
|
tickCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Lifecycle ---
|
// --- Lifecycle ---
|
||||||
|
|
@ -208,6 +262,8 @@ export function usePetSystem() {
|
||||||
clean,
|
clean,
|
||||||
sleep,
|
sleep,
|
||||||
hatchEgg,
|
hatchEgg,
|
||||||
reset
|
reset,
|
||||||
|
achievements,
|
||||||
|
unlockAllAchievements
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue