feat: add func
This commit is contained in:
parent
d0e1bb91de
commit
c3d07770be
161
src/App.vue
161
src/App.vue
|
|
@ -4,42 +4,68 @@ import DeviceShell from './components/DeviceShell.vue';
|
||||||
import DeviceScreen from './components/DeviceScreen.vue';
|
import DeviceScreen from './components/DeviceScreen.vue';
|
||||||
import PetGame from './components/PetGame.vue';
|
import PetGame from './components/PetGame.vue';
|
||||||
import Menu from './components/Menu.vue';
|
import Menu from './components/Menu.vue';
|
||||||
|
import { usePetSystem } from './composables/usePetSystem';
|
||||||
|
|
||||||
const currentScreen = ref('game');
|
const currentScreen = ref('game');
|
||||||
const currentState = ref('idle');
|
|
||||||
const currentStage = ref('adult'); // 'egg' or 'adult'
|
|
||||||
const petGameRef = ref(null);
|
const petGameRef = ref(null);
|
||||||
|
const showStats = ref(false); // Stats visibility
|
||||||
|
|
||||||
// State Management
|
// Initialize Pet System
|
||||||
function setPetState(state) {
|
const {
|
||||||
currentState.value = state;
|
stage,
|
||||||
|
state,
|
||||||
|
stats,
|
||||||
|
feed,
|
||||||
|
play,
|
||||||
|
sleep,
|
||||||
|
clean,
|
||||||
|
isCleaning,
|
||||||
|
hatchEgg,
|
||||||
|
reset
|
||||||
|
} = usePetSystem();
|
||||||
|
|
||||||
|
// Handle Action Menu Events
|
||||||
|
function handleAction(action) {
|
||||||
|
switch(action) {
|
||||||
|
case 'feed':
|
||||||
|
feed();
|
||||||
|
break;
|
||||||
|
case 'clean':
|
||||||
|
clean();
|
||||||
|
break;
|
||||||
|
case 'play':
|
||||||
|
play();
|
||||||
|
break;
|
||||||
|
case 'sleep':
|
||||||
|
sleep();
|
||||||
|
break;
|
||||||
|
case 'medicine':
|
||||||
|
// Heal the pet
|
||||||
|
if (state.value === 'sick') {
|
||||||
|
stats.value.health = 100;
|
||||||
|
state.value = 'idle';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'stats':
|
||||||
|
// Toggle stats display
|
||||||
|
showStats.value = !showStats.value;
|
||||||
|
break;
|
||||||
|
case 'settings':
|
||||||
|
// Show reset options
|
||||||
|
if (stage.value === 'egg') {
|
||||||
|
hatchEgg();
|
||||||
|
} else {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log('Action not implemented:', action);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStateUpdate(newState) {
|
// Debug/Dev Controls
|
||||||
currentState.value = newState;
|
function setPetState(newState) {
|
||||||
}
|
state.value = newState;
|
||||||
|
|
||||||
function toggleStage() {
|
|
||||||
currentStage.value = currentStage.value === 'egg' ? 'adult' : 'egg';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Feeding Logic
|
|
||||||
const stateBeforeFeeding = ref('idle');
|
|
||||||
|
|
||||||
function startFeeding() {
|
|
||||||
if (currentState.value === 'sleep' || currentState.value === 'dead') return;
|
|
||||||
|
|
||||||
if (currentState.value === 'sick') {
|
|
||||||
if (petGameRef.value && petGameRef.value.shakeHead) {
|
|
||||||
petGameRef.value.shakeHead();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentState.value === 'idle') {
|
|
||||||
stateBeforeFeeding.value = currentState.value;
|
|
||||||
currentState.value = 'eating';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -48,34 +74,83 @@ function startFeeding() {
|
||||||
<DeviceScreen>
|
<DeviceScreen>
|
||||||
<!-- Dynamic Component Switching -->
|
<!-- Dynamic Component Switching -->
|
||||||
<PetGame
|
<PetGame
|
||||||
|
v-if="currentScreen === 'game'"
|
||||||
ref="petGameRef"
|
ref="petGameRef"
|
||||||
:state="currentState"
|
:state="state"
|
||||||
:stage="currentStage"
|
:stage="stage"
|
||||||
@update:state="handleStateUpdate"
|
:stats="stats"
|
||||||
|
:isCleaning="isCleaning"
|
||||||
|
:showStats="showStats"
|
||||||
|
@update:state="state = $event"
|
||||||
|
@action="handleAction"
|
||||||
/>
|
/>
|
||||||
|
<Menu v-else />
|
||||||
</DeviceScreen>
|
</DeviceScreen>
|
||||||
</DeviceShell>
|
</DeviceShell>
|
||||||
|
|
||||||
<!-- Controls (Outside Machine) -->
|
<!-- Debug Controls (Outside Device) -->
|
||||||
<div class="controls">
|
<div class="debug-controls">
|
||||||
|
<div class="control-section">
|
||||||
|
<h3>Debug Controls</h3>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button @click="setPetState('idle')" :disabled="currentStage === 'egg'">Idle</button>
|
<button @click="setPetState('idle')">Idle</button>
|
||||||
<button @click="setPetState('sleep')" :disabled="currentStage === 'egg'">Sleep</button>
|
<button @click="setPetState('sleep')">Sleep</button>
|
||||||
<button @click="setPetState('sick')" :disabled="currentStage === 'egg'">Sick</button>
|
<button @click="setPetState('sick')">Sick</button>
|
||||||
<button @click="setPetState('dead')" :disabled="currentStage === 'egg'">Dead</button>
|
<button @click="setPetState('dead')">Dead</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button @click="startFeeding" :disabled="currentStage === 'egg'">Feed</button>
|
<button @click="stats.poopCount = Math.min((stats.poopCount || 0) + 1, 4)">💩 Add Poop</button>
|
||||||
<button @click="toggleStage">{{ currentStage === 'egg' ? 'Hatch' : 'Reset to Egg' }}</button>
|
<button @click="stats.poopCount = 0">🧼 Clean All</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button @click="currentScreen = 'game'" :disabled="currentStage === 'egg'">Game</button>
|
<button v-if="stage === 'egg'" @click="hatchEgg()">🥚 Hatch Egg</button>
|
||||||
<button @click="currentScreen = 'menu'" :disabled="currentStage === 'egg'">Menu</button>
|
<button v-else @click="reset()">🔄 Reset to Egg</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.debug-controls {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 400px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-section h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group button:hover {
|
||||||
|
background: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group button:active {
|
||||||
|
background: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
<template>
|
||||||
|
<div class="action-menu">
|
||||||
|
<button class="icon-btn icon-clean" @click="$emit('clean')" :disabled="disabled || poopCount === 0" title="Clean"></button>
|
||||||
|
<button class="icon-btn icon-medicine" @click="$emit('medicine')" :disabled="disabled || !isSick" title="Medicine"></button>
|
||||||
|
<button class="icon-btn icon-training" @click="$emit('training')" :disabled="disabled" title="Training"></button>
|
||||||
|
<button class="icon-btn icon-info" @click="$emit('info')" :disabled="disabled" title="Info"></button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
poopCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
health: {
|
||||||
|
type: Number,
|
||||||
|
default: 100
|
||||||
|
},
|
||||||
|
isSick: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(['clean', 'medicine', 'training', 'info']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.action-menu {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: rgba(155, 188, 15, 0.05);
|
||||||
|
border-top: 2px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clean Icon (Broom) */
|
||||||
|
.icon-clean::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #8B4513;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow:
|
||||||
|
0px -6px 0 #8B4513, 0px -4px 0 #8B4513,
|
||||||
|
0px -2px 0 #8B4513, 0px 0px 0 #8B4513,
|
||||||
|
-4px 2px 0 #ffcc00, -2px 2px 0 #ffcc00, 0px 2px 0 #ffcc00, 2px 2px 0 #ffcc00, 4px 2px 0 #ffcc00,
|
||||||
|
-4px 4px 0 #ffcc00, -2px 4px 0 #ffcc00, 0px 4px 0 #ffcc00, 2px 4px 0 #ffcc00, 4px 4px 0 #ffcc00,
|
||||||
|
-2px 6px 0 #ffcc00, 0px 6px 0 #ffcc00, 2px 6px 0 #ffcc00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Medicine Icon (Pill/Cross) */
|
||||||
|
.icon-medicine::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #ff4444;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow:
|
||||||
|
0px -6px 0 #ff4444, 0px -4px 0 #ff4444,
|
||||||
|
-4px -2px 0 #ff4444, -2px -2px 0 #ff4444, 0px -2px 0 #ff4444, 2px -2px 0 #ff4444, 4px -2px 0 #ff4444,
|
||||||
|
0px 0px 0 #ff4444, 0px 2px 0 #ff4444,
|
||||||
|
0px 4px 0 #ff4444, 0px 6px 0 #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Training Icon (Dumbbell) */
|
||||||
|
.icon-training::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #444;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow:
|
||||||
|
-6px -2px 0 #444, -6px 0px 0 #444, -6px 2px 0 #444,
|
||||||
|
-4px -2px 0 #444, -4px 0px 0 #444, -4px 2px 0 #444,
|
||||||
|
-2px 0px 0 #444, 0px 0px 0 #444, 2px 0px 0 #444,
|
||||||
|
4px -2px 0 #444, 4px 0px 0 #444, 4px 2px 0 #444,
|
||||||
|
6px -2px 0 #444, 6px 0px 0 #444, 6px 2px 0 #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info Icon (i) */
|
||||||
|
.icon-info::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #4444ff;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow:
|
||||||
|
0px -6px 0 #4444ff,
|
||||||
|
0px -2px 0 #4444ff, 0px 0px 0 #4444ff,
|
||||||
|
0px 2px 0 #4444ff, 0px 4px 0 #4444ff, 0px 6px 0 #4444ff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,5 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
|
<div class="pet-game-wrapper">
|
||||||
|
<!-- Top Menu -->
|
||||||
|
<TopMenu
|
||||||
|
:disabled="stage === 'egg'"
|
||||||
|
@stats="$emit('action', 'stats')"
|
||||||
|
@feed="$emit('action', 'feed')"
|
||||||
|
@play="$emit('action', 'play')"
|
||||||
|
@sleep="$emit('action', 'sleep')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Stats Dashboard (Toggelable) -->
|
||||||
|
<StatsBar
|
||||||
|
v-if="showStats"
|
||||||
|
:hunger="stats?.hunger || 100"
|
||||||
|
:happiness="stats?.happiness || 100"
|
||||||
|
:health="stats?.health || 100"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Game Area (Center) -->
|
||||||
<div class="pet-game-container" ref="containerRef">
|
<div class="pet-game-container" ref="containerRef">
|
||||||
|
<!-- 關燈黑色遮罩 -->
|
||||||
|
<div
|
||||||
|
v-if="state === 'sleep'"
|
||||||
|
class="dark-overlay"
|
||||||
|
></div>
|
||||||
|
|
||||||
<!-- 寵物本體 -->
|
<!-- 寵物本體 -->
|
||||||
<div
|
<div
|
||||||
class="pet-root"
|
class="pet-root"
|
||||||
|
|
@ -9,9 +34,10 @@
|
||||||
top: petY + 'px',
|
top: petY + 'px',
|
||||||
width: width + 'px',
|
width: width + 'px',
|
||||||
height: height + 'px',
|
height: height + 'px',
|
||||||
display: state === 'dead' ? 'none' : 'block'
|
display: (state === 'dead' || state === 'sleep') ? 'none' : 'block',
|
||||||
|
zIndex: 10
|
||||||
}"
|
}"
|
||||||
:class="['state-' + state, { 'shaking-head': isShakingHead }]"
|
:class="['state-' + state, 'stage-' + stage, { 'shaking-head': isShakingHead }]"
|
||||||
>
|
>
|
||||||
<div class="pet-inner" :class="isFacingRight ? 'face-right' : 'face-left'">
|
<div class="pet-inner" :class="isFacingRight ? 'face-right' : 'face-left'">
|
||||||
<!-- 根據是否張嘴選擇顯示的像素 -->
|
<!-- 根據是否張嘴選擇顯示的像素 -->
|
||||||
|
|
@ -56,7 +82,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 睡覺 ZZZ -->
|
<!-- 睡覺 ZZZ -->
|
||||||
<div class="sleep-zzz" :style="iconStyle" v-show="state === 'sleep'">
|
<div
|
||||||
|
class="sleep-zzz"
|
||||||
|
:class="{ 'dark-mode': state === 'sleep' }"
|
||||||
|
:style="iconStyle"
|
||||||
|
v-show="state === 'sleep'"
|
||||||
|
>
|
||||||
<span class="z1">Z</span>
|
<span class="z1">Z</span>
|
||||||
<span class="z2">Z</span>
|
<span class="z2">Z</span>
|
||||||
<span class="z3">Z</span>
|
<span class="z3">Z</span>
|
||||||
|
|
@ -68,10 +99,48 @@
|
||||||
<!-- 死亡墓碑 -->
|
<!-- 死亡墓碑 -->
|
||||||
<div class="tombstone" v-show="state === 'dead'"></div>
|
<div class="tombstone" v-show="state === 'dead'"></div>
|
||||||
|
|
||||||
<!-- Debug Overlay -->
|
<!-- 便便 (Poop) - Scattered on sides -->
|
||||||
<div class="debug-overlay">
|
<div
|
||||||
|
v-for="i in Math.min(stats?.poopCount || 0, 4)"
|
||||||
|
:key="'poop-' + i"
|
||||||
|
class="poop"
|
||||||
|
:class="{ 'flushing': isCleaning }"
|
||||||
|
:style="getPoopPosition(i)"
|
||||||
|
>
|
||||||
|
<div class="poop-sprite"></div>
|
||||||
|
<div class="poop-stink">
|
||||||
|
<span class="stink-line s1"></span>
|
||||||
|
<span class="stink-line s2"></span>
|
||||||
|
<span class="stink-line s3"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Debug Overlay (Hidden) -->
|
||||||
|
<div class="debug-overlay" v-if="false">
|
||||||
{{ containerWidth }}x{{ containerHeight }} | {{ Math.round(petX) }},{{ Math.round(petY) }} | {{ state }}
|
{{ containerWidth }}x{{ containerHeight }} | {{ Math.round(petX) }},{{ Math.round(petY) }} | {{ state }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Flush Animation - Single wave covering all poop areas -->
|
||||||
|
<div
|
||||||
|
v-if="isCleaning && (stats?.poopCount || 0) > 0"
|
||||||
|
class="flush-wave"
|
||||||
|
:style="getFlushAreaStyle()"
|
||||||
|
>
|
||||||
|
<div class="wave-drop"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Menu (Bottom) -->
|
||||||
|
<ActionMenu
|
||||||
|
:disabled="stage === 'egg'"
|
||||||
|
:poopCount="stats?.poopCount || 0"
|
||||||
|
:health="stats?.health || 100"
|
||||||
|
:isSick="state === 'sick'"
|
||||||
|
@clean="$emit('action', 'clean')"
|
||||||
|
@medicine="$emit('action', 'medicine')"
|
||||||
|
@training="$emit('action', 'training')"
|
||||||
|
@info="$emit('action', 'info')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -79,6 +148,9 @@
|
||||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||||
import { SPRITE_PRESETS } from '../data/petPresets.js';
|
import { SPRITE_PRESETS } from '../data/petPresets.js';
|
||||||
import { FOOD_OPTIONS } from '../data/foodOptions.js';
|
import { FOOD_OPTIONS } from '../data/foodOptions.js';
|
||||||
|
import StatsBar from './StatsBar.vue';
|
||||||
|
import ActionMenu from './ActionMenu.vue';
|
||||||
|
import TopMenu from './TopMenu.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
state: {
|
state: {
|
||||||
|
|
@ -88,10 +160,90 @@ const props = defineProps({
|
||||||
stage: {
|
stage: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'adult' // 'egg' or 'adult'
|
default: 'adult' // 'egg' or 'adult'
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ hunger: 100, happiness: 100, health: 100, poopCount: 0 })
|
||||||
|
},
|
||||||
|
showStats: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
isCleaning: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:state']);
|
const emit = defineEmits(['update:state', 'action']);
|
||||||
|
|
||||||
|
// Stats visibility toggle (removed local ref, using prop instead)
|
||||||
|
|
||||||
|
// Poop position calculator (all on left side, strictly in game area)
|
||||||
|
function getPoopPosition(index) {
|
||||||
|
const positions = [
|
||||||
|
{ left: '10px', bottom: '15px' }, // Bottom left
|
||||||
|
{ left: '45px', bottom: '15px' }, // Bottom left-center
|
||||||
|
{ left: '10px', bottom: '50px' }, // Mid left
|
||||||
|
{ left: '45px', bottom: '50px' } // Mid left-center
|
||||||
|
];
|
||||||
|
return positions[index - 1] || positions[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poop area boundaries (for collision detection)
|
||||||
|
// Each poop area is 32px x 32px
|
||||||
|
const POOP_AREA_SIZE = 32;
|
||||||
|
function getPoopAreas() {
|
||||||
|
const poopCount = props.stats?.poopCount || 0;
|
||||||
|
if (poopCount === 0) return []; // No poop, return empty array
|
||||||
|
|
||||||
|
const positions = [
|
||||||
|
{ left: 10, bottom: 15 }, // Index 1: Bottom left
|
||||||
|
{ left: 45, bottom: 15 }, // Index 2: Bottom left-center
|
||||||
|
{ left: 10, bottom: 50 }, // Index 3: Mid left
|
||||||
|
{ left: 45, bottom: 50 } // Index 4: Mid left-center
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!containerRef.value) return [];
|
||||||
|
const ch = containerRef.value.clientHeight;
|
||||||
|
|
||||||
|
// Only return areas that actually have poop (based on poopCount)
|
||||||
|
return positions.slice(0, Math.min(poopCount, 4)).map(pos => ({
|
||||||
|
left: pos.left,
|
||||||
|
right: pos.left + POOP_AREA_SIZE,
|
||||||
|
top: ch - pos.bottom - POOP_AREA_SIZE,
|
||||||
|
bottom: ch - pos.bottom
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a position overlaps with any poop area (only if there's actual poop)
|
||||||
|
function isInPoopArea(x, y, w, h) {
|
||||||
|
const areas = getPoopAreas();
|
||||||
|
if (areas.length === 0) return false; // No poop, can move anywhere
|
||||||
|
return areas.some(area => {
|
||||||
|
return !(x + w < area.left || x > area.right || y + h < area.top || y > area.bottom);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get style for flush area covering all poop positions
|
||||||
|
function getFlushAreaStyle() {
|
||||||
|
if (!containerRef.value) return {};
|
||||||
|
const ch = containerRef.value.clientHeight;
|
||||||
|
|
||||||
|
// Calculate bounding box for all poop areas
|
||||||
|
// Poop positions: left 10px, 45px; bottom 15px, 50px
|
||||||
|
const minLeft = 10;
|
||||||
|
const maxRight = 45 + POOP_AREA_SIZE; // 45 + 32 = 77
|
||||||
|
const minTop = ch - 50 - POOP_AREA_SIZE; // Top of highest poop (bottom: 50)
|
||||||
|
const maxBottom = ch - 15; // Bottom of lowest poop (bottom: 15)
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: minLeft + 'px',
|
||||||
|
top: '0px', // Start from top of screen
|
||||||
|
width: (maxRight - minLeft) + 'px',
|
||||||
|
height: maxBottom + 'px'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Default food choice
|
// Default food choice
|
||||||
const currentFood = 'banana'; // Can be: 'apple', 'banana', 'strawberry'
|
const currentFood = 'banana'; // Can be: 'apple', 'banana', 'strawberry'
|
||||||
|
|
@ -165,10 +317,20 @@ 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);
|
||||||
|
|
||||||
|
// Animation State
|
||||||
|
const isBlinking = ref(false);
|
||||||
|
|
||||||
const currentPixels = computed(() => {
|
const currentPixels = computed(() => {
|
||||||
|
// Priority: Egg > Blink > Mouth Open > Normal
|
||||||
if (props.stage === 'egg') {
|
if (props.stage === 'egg') {
|
||||||
return generatePixels(CURRENT_PRESET.eggSprite, CURRENT_PRESET.eggPalette);
|
return generatePixels(CURRENT_PRESET.eggSprite, CURRENT_PRESET.eggPalette);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Blink overrides mouth state (when blinking, always show closed eyes)
|
||||||
|
if (isBlinking.value && CURRENT_PRESET.spriteEyesClosed) {
|
||||||
|
return generatePixels(CURRENT_PRESET.spriteEyesClosed);
|
||||||
|
}
|
||||||
|
|
||||||
return isMouthOpen.value
|
return isMouthOpen.value
|
||||||
? generatePixels(CURRENT_PRESET.spriteMouthOpen)
|
? generatePixels(CURRENT_PRESET.spriteMouthOpen)
|
||||||
: generatePixels(CURRENT_PRESET.sprite);
|
: generatePixels(CURRENT_PRESET.sprite);
|
||||||
|
|
@ -263,6 +425,23 @@ function moveRandomly() {
|
||||||
newX = Math.max(minX, Math.min(maxX, newX));
|
newX = Math.max(minX, Math.min(maxX, newX));
|
||||||
newY = Math.max(minY, Math.min(maxY, newY));
|
newY = Math.max(minY, Math.min(maxY, newY));
|
||||||
|
|
||||||
|
// Check if new position would overlap with poop areas (only if there's actual poop)
|
||||||
|
if (isInPoopArea(newX, newY, width, height)) {
|
||||||
|
// If overlapping, try to move away from poop areas
|
||||||
|
// Prefer moving right or up to avoid poop areas on the left
|
||||||
|
if (newX < 80) {
|
||||||
|
newX = Math.max(80, newX); // Move right of poop areas
|
||||||
|
}
|
||||||
|
// If still overlapping, try moving up
|
||||||
|
if (isInPoopArea(newX, newY, width, height)) {
|
||||||
|
const areas = getPoopAreas();
|
||||||
|
if (areas.length > 0) {
|
||||||
|
const maxPoopBottom = Math.max(...areas.map(a => a.bottom));
|
||||||
|
newY = Math.max(minY, Math.min(newY, maxPoopBottom - height - 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dx = newX - petX.value;
|
const dx = newX - petX.value;
|
||||||
if (dx > 0) isFacingRight.value = false;
|
if (dx > 0) isFacingRight.value = false;
|
||||||
else if (dx < 0) isFacingRight.value = true;
|
else if (dx < 0) isFacingRight.value = true;
|
||||||
|
|
@ -286,12 +465,31 @@ async function startFeeding() {
|
||||||
const mouthY = 8.5; // Mouth is at row 8-9
|
const mouthY = 8.5; // Mouth is at row 8-9
|
||||||
|
|
||||||
// Set horizontal position (in front of pet)
|
// Set horizontal position (in front of pet)
|
||||||
foodX.value = petX.value + frontOffsetX;
|
let targetFoodX = petX.value + frontOffsetX;
|
||||||
|
|
||||||
|
// Only avoid poop areas if there's actual poop
|
||||||
|
const areas = getPoopAreas();
|
||||||
|
if (areas.length > 0) {
|
||||||
|
const maxPoopRight = Math.max(...areas.map(a => a.right));
|
||||||
|
if (targetFoodX < maxPoopRight + 10) {
|
||||||
|
// Move food to the right of poop areas
|
||||||
|
targetFoodX = maxPoopRight + 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foodX.value = targetFoodX;
|
||||||
foodY.value = 0; // Start from top of screen
|
foodY.value = 0; // Start from top of screen
|
||||||
|
|
||||||
// Calculate target Y (at mouth level)
|
// Calculate target Y (at mouth level)
|
||||||
const targetY = petY.value + (mouthY * pixelSize) - (foodSize / 2);
|
const targetY = petY.value + (mouthY * pixelSize) - (foodSize / 2);
|
||||||
|
|
||||||
|
// Only avoid poop areas vertically if there's actual poop
|
||||||
|
let safeTargetY = targetY;
|
||||||
|
if (areas.length > 0) {
|
||||||
|
const maxPoopBottom = Math.max(...areas.map(a => a.bottom));
|
||||||
|
safeTargetY = Math.min(targetY, maxPoopBottom - foodSize - 5);
|
||||||
|
}
|
||||||
|
|
||||||
// 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();
|
||||||
|
|
@ -302,7 +500,7 @@ async function startFeeding() {
|
||||||
const progress = Math.min(elapsed / duration, 1);
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
// Ease out for smoother landing
|
// Ease out for smoother landing
|
||||||
const eased = 1 - Math.pow(1 - progress, 3);
|
const eased = 1 - Math.pow(1 - progress, 3);
|
||||||
foodY.value = eased * targetY;
|
foodY.value = eased * safeTargetY;
|
||||||
|
|
||||||
if (progress < 1) {
|
if (progress < 1) {
|
||||||
requestAnimationFrame(animateFall);
|
requestAnimationFrame(animateFall);
|
||||||
|
|
@ -341,7 +539,9 @@ async function startFeeding() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize position
|
// Initialize position
|
||||||
|
let intervalId;
|
||||||
let resizeObserver;
|
let resizeObserver;
|
||||||
|
let blinkTimeoutId;
|
||||||
|
|
||||||
function initPosition() {
|
function initPosition() {
|
||||||
if (containerRef.value) {
|
if (containerRef.value) {
|
||||||
|
|
@ -351,8 +551,25 @@ function initPosition() {
|
||||||
containerHeight.value = ch;
|
containerHeight.value = ch;
|
||||||
|
|
||||||
if (cw > 0 && ch > 0) {
|
if (cw > 0 && ch > 0) {
|
||||||
petX.value = Math.floor(cw / 2 - width / 2);
|
let initialX = Math.floor(cw / 2 - width / 2);
|
||||||
petY.value = Math.floor(ch / 2 - height / 2);
|
let initialY = Math.floor(ch / 2 - height / 2);
|
||||||
|
|
||||||
|
// Only avoid poop areas if there's actual poop
|
||||||
|
if (isInPoopArea(initialX, initialY, width, height)) {
|
||||||
|
// Move to the right of poop areas
|
||||||
|
initialX = Math.max(80, initialX);
|
||||||
|
// If still overlapping, move up
|
||||||
|
if (isInPoopArea(initialX, initialY, width, height)) {
|
||||||
|
const areas = getPoopAreas();
|
||||||
|
if (areas.length > 0) {
|
||||||
|
const maxPoopBottom = Math.max(...areas.map(a => a.bottom));
|
||||||
|
initialY = Math.max(10, Math.min(initialY, maxPoopBottom - height - 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
petX.value = initialX;
|
||||||
|
petY.value = initialY;
|
||||||
updateHeadIconsPosition();
|
updateHeadIconsPosition();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -382,11 +599,28 @@ onMounted(async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
intervalId = setInterval(moveRandomly, 600);
|
intervalId = setInterval(moveRandomly, 600);
|
||||||
|
|
||||||
|
// Blink Animation Timer
|
||||||
|
function scheduleBlink() {
|
||||||
|
const nextBlinkDelay = 3000 + Math.random() * 2000; // 3-5 seconds
|
||||||
|
blinkTimeoutId = setTimeout(() => {
|
||||||
|
// Don't blink when eating, dead, or egg
|
||||||
|
if (props.state !== 'eating' && props.state !== 'dead' && props.stage !== 'egg') {
|
||||||
|
isBlinking.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
isBlinking.value = false;
|
||||||
|
}, 150); // Blink duration: 150ms
|
||||||
|
}
|
||||||
|
scheduleBlink(); // Schedule next blink
|
||||||
|
}, nextBlinkDelay);
|
||||||
|
}
|
||||||
|
scheduleBlink(); // Start blinking
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
if (resizeObserver) resizeObserver.disconnect();
|
if (resizeObserver) resizeObserver.disconnect();
|
||||||
|
if (blinkTimeoutId) clearTimeout(blinkTimeoutId);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch state changes to update icon position immediately
|
// Watch state changes to update icon position immediately
|
||||||
|
|
@ -433,12 +667,20 @@ defineExpose({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.pet-game-container {
|
.pet-game-wrapper {
|
||||||
position: relative;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pet-game-container {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.debug-overlay {
|
.debug-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
|
|
@ -453,7 +695,7 @@ defineExpose({
|
||||||
.pet-root {
|
.pet-root {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform-origin: center bottom;
|
transform-origin: center bottom;
|
||||||
z-index: 5;
|
z-index: 10; /* Higher than water and other elements */
|
||||||
}
|
}
|
||||||
|
|
||||||
.pet-inner {
|
.pet-inner {
|
||||||
|
|
@ -588,13 +830,29 @@ defineExpose({
|
||||||
50% { opacity: 1; }
|
50% { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark Overlay (關燈效果) */
|
||||||
|
.dark-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #000;
|
||||||
|
z-index: 7; /* Above poop (5), Below ZZZ (10) */
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Sleep ZZZ */
|
/* Sleep ZZZ */
|
||||||
.sleep-zzz {
|
.sleep-zzz {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #5a7a4a;
|
color: #5a7a4a;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10; /* Above dark overlay */
|
||||||
|
}
|
||||||
|
.sleep-zzz.dark-mode {
|
||||||
|
color: #fff; /* 顏色顛倒:在黑色背景下變白色 */
|
||||||
|
text-shadow: 0 0 4px rgba(255, 255, 255, 0.8); /* 添加發光效果讓它更明顯 */
|
||||||
}
|
}
|
||||||
.sleep-zzz span {
|
.sleep-zzz span {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -627,36 +885,225 @@ defineExpose({
|
||||||
50% { transform: translateY(-2px); }
|
50% { transform: translateY(-2px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tombstone */
|
/* Tombstone (Dead State) */
|
||||||
.tombstone {
|
.tombstone {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 52%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: 48px;
|
width: 30px;
|
||||||
height: 56px;
|
height: 40px;
|
||||||
border-radius: 16px 16px 6px 6px;
|
background: linear-gradient(to bottom, #888 0%, #555 100%);
|
||||||
background: #9ba7a0;
|
border-radius: 10px 10px 0 0;
|
||||||
border: 2px solid #5e6861;
|
border: 2px solid #333;
|
||||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.2), 0 4px 4px rgba(0, 0, 0, 0.35);
|
|
||||||
z-index: 10;
|
|
||||||
animation: tomb-float 3s ease-in-out infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tombstone::before {
|
.tombstone::before {
|
||||||
content: "";
|
content: 'RIP';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 8px; right: 8px; top: 16px;
|
top: 50%;
|
||||||
height: 2px;
|
left: 50%;
|
||||||
background: #5e6861;
|
transform: translate(-50%, -50%);
|
||||||
}
|
|
||||||
.tombstone::after {
|
|
||||||
content: "RIP";
|
|
||||||
position: absolute;
|
|
||||||
top: 22px; left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
letter-spacing: 1px;
|
font-weight: bold;
|
||||||
color: #39413c;
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Poop Sprite (Larger & More Detailed) */
|
||||||
|
.poop {
|
||||||
|
position: absolute;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poop-sprite {
|
||||||
|
position: relative;
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
background: #3d2817;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow:
|
||||||
|
/* Top point */
|
||||||
|
0px -12px 0 #3d2817,
|
||||||
|
/* Row 2 - narrow */
|
||||||
|
-3px -9px 0 #3d2817, 0px -9px 0 #5a4028, 3px -9px 0 #3d2817,
|
||||||
|
/* Row 3 */
|
||||||
|
-6px -6px 0 #3d2817, -3px -6px 0 #5a4028, 0px -6px 0 #6b4e38,
|
||||||
|
3px -6px 0 #5a4028, 6px -6px 0 #3d2817,
|
||||||
|
/* Row 4 - eyes */
|
||||||
|
-6px -3px 0 #3d2817, -3px -3px 0 #ffffff, 0px -3px 0 #6b4e38,
|
||||||
|
3px -3px 0 #ffffff, 6px -3px 0 #3d2817,
|
||||||
|
/* Row 5 - middle */
|
||||||
|
-9px 0px 0 #3d2817, -6px 0px 0 #5a4028, -3px 0px 0 #6b4e38, 0px 0px 0 #7d5a3a,
|
||||||
|
3px 0px 0 #6b4e38, 6px 0px 0 #5a4028, 9px 0px 0 #3d2817,
|
||||||
|
/* Row 6 */
|
||||||
|
-9px 3px 0 #3d2817, -6px 3px 0 #5a4028, -3px 3px 0 #6b4e38, 0px 3px 0 #7d5a3a,
|
||||||
|
3px 3px 0 #6b4e38, 6px 3px 0 #5a4028, 9px 3px 0 #3d2817,
|
||||||
|
/* Row 7 */
|
||||||
|
-12px 6px 0 #3d2817, -9px 6px 0 #3d2817, -6px 6px 0 #5a4028, -3px 6px 0 #6b4e38,
|
||||||
|
0px 6px 0 #7d5a3a, 3px 6px 0 #6b4e38, 6px 6px 0 #5a4028, 9px 6px 0 #3d2817, 12px 6px 0 #3d2817,
|
||||||
|
/* Row 8 - wider */
|
||||||
|
-12px 9px 0 #3d2817, -9px 9px 0 #5a4028, -6px 9px 0 #6b4e38, -3px 9px 0 #7d5a3a,
|
||||||
|
0px 9px 0 #7d5a3a, 3px 9px 0 #7d5a3a, 6px 9px 0 #6b4e38, 9px 9px 0 #5a4028, 12px 9px 0 #3d2817,
|
||||||
|
/* Bottom row */
|
||||||
|
-9px 12px 0 #3d2817, -6px 12px 0 #5a4028, -3px 12px 0 #6b4e38,
|
||||||
|
0px 12px 0 #6b4e38, 3px 12px 0 #6b4e38, 6px 12px 0 #5a4028, 9px 12px 0 #3d2817;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stink Animation (3 Fingers Style - Wavy) */
|
||||||
|
.poop-stink {
|
||||||
|
position: absolute;
|
||||||
|
top: -25px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 40px;
|
||||||
|
height: 30px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stink-line {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: transparent;
|
||||||
|
/* Pixel art vertical wave pattern */
|
||||||
|
box-shadow:
|
||||||
|
0px 0px 0 #555,
|
||||||
|
1px -2px 0 #555,
|
||||||
|
1px -4px 0 #555,
|
||||||
|
0px -6px 0 #555,
|
||||||
|
-1px -8px 0 #555,
|
||||||
|
-1px -10px 0 #555,
|
||||||
|
0px -12px 0 #555;
|
||||||
|
opacity: 0;
|
||||||
|
transform-origin: bottom center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left Finger */
|
||||||
|
.stink-line.s1 {
|
||||||
|
animation: stink-finger-1 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Middle Finger */
|
||||||
|
.stink-line.s2 {
|
||||||
|
/* Slightly taller wave for middle */
|
||||||
|
box-shadow:
|
||||||
|
0px 0px 0 #555,
|
||||||
|
1px -2px 0 #555,
|
||||||
|
1px -4px 0 #555,
|
||||||
|
0px -6px 0 #555,
|
||||||
|
-1px -8px 0 #555,
|
||||||
|
-1px -10px 0 #555,
|
||||||
|
0px -12px 0 #555,
|
||||||
|
1px -14px 0 #555;
|
||||||
|
animation: stink-finger-2 2s ease-in-out infinite;
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right Finger */
|
||||||
|
.stink-line.s3 {
|
||||||
|
animation: stink-finger-3 2s ease-in-out infinite;
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes stink-finger-1 {
|
||||||
|
0% { opacity: 0; transform: translateX(-50%) rotate(-25deg) scaleY(0.5); }
|
||||||
|
20% { opacity: 0.8; transform: translateX(-50%) rotate(-25deg) scaleY(1) translateY(-5px); }
|
||||||
|
80% { opacity: 0.4; transform: translateX(-50%) rotate(-35deg) scaleY(1) translateY(-15px); }
|
||||||
|
100% { opacity: 0; transform: translateX(-50%) rotate(-40deg) scaleY(1.2) translateY(-20px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes stink-finger-2 {
|
||||||
|
0% { opacity: 0; transform: translateX(-50%) rotate(0deg) scaleY(0.5); }
|
||||||
|
20% { opacity: 0.8; transform: translateX(-50%) rotate(0deg) scaleY(1) translateY(-6px); }
|
||||||
|
80% { opacity: 0.4; transform: translateX(-50%) rotate(0deg) scaleY(1) translateY(-18px); }
|
||||||
|
100% { opacity: 0; transform: translateX(-50%) rotate(0deg) scaleY(1.2) translateY(-24px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes stink-finger-3 {
|
||||||
|
0% { opacity: 0; transform: translateX(-50%) rotate(25deg) scaleY(0.5); }
|
||||||
|
20% { opacity: 0.8; transform: translateX(-50%) rotate(25deg) scaleY(1) translateY(-5px); }
|
||||||
|
80% { opacity: 0.4; transform: translateX(-50%) rotate(35deg) scaleY(1) translateY(-15px); }
|
||||||
|
100% { opacity: 0; transform: translateX(-50%) rotate(40deg) scaleY(1.2) translateY(-20px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Poop Flush Animation */
|
||||||
|
.poop.flushing .poop-stink {
|
||||||
|
display: none; /* Hide stink immediately when flushing */
|
||||||
|
}
|
||||||
|
|
||||||
|
.poop.flushing .poop-sprite {
|
||||||
|
animation: poop-flush 1.5s ease-in-out forwards;
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes poop-flush {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, -50%) rotate(0deg) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%) rotate(720deg) scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flush Animation (Pixel Style - Top & Bottom) */
|
||||||
|
/* Flush Wave - Single wave covering all poop areas, drops from top */
|
||||||
|
.flush-wave {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 6; /* Above poop (5), Below pet (10) */
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-drop {
|
||||||
|
position: absolute;
|
||||||
|
top: -100%;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #40a4ff;
|
||||||
|
animation: flush-drop 1.5s ease-out forwards;
|
||||||
|
/* Pixel pattern */
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, #60b4ff 25%, transparent 25%, transparent 75%, #60b4ff 75%, #60b4ff),
|
||||||
|
linear-gradient(45deg, #60b4ff 25%, transparent 25%, transparent 75%, #60b4ff 75%, #60b4ff);
|
||||||
|
background-size: 8px 8px;
|
||||||
|
background-position: 0 0, 4px 4px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 2px 0 #fff,
|
||||||
|
inset 0 -2px 0 #2a8fdd,
|
||||||
|
0 0 4px rgba(64, 164, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flush-drop {
|
||||||
|
0% {
|
||||||
|
top: -100%;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Egg breathe animation */
|
||||||
|
.pet-root.stage-egg .pet-inner {
|
||||||
|
animation: egg-breathe 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egg-breathe {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.03); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes tomb-float {
|
@keyframes tomb-float {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
<template>
|
||||||
|
<div class="stats-bar">
|
||||||
|
<div class="stat-row">
|
||||||
|
<div class="stat-icon pixel-heart"></div>
|
||||||
|
<div class="pixel-bar">
|
||||||
|
<div
|
||||||
|
v-for="i in 10"
|
||||||
|
:key="'happy-' + i"
|
||||||
|
class="pixel-block"
|
||||||
|
:class="{
|
||||||
|
'filled': i <= Math.floor(happiness / 10),
|
||||||
|
'color-red': i <= Math.floor(happiness / 10)
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span class="stat-value" :class="{ 'warning': happiness < 30 }">{{ happiness }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-row">
|
||||||
|
<div class="stat-icon pixel-food"></div>
|
||||||
|
<div class="pixel-bar">
|
||||||
|
<div
|
||||||
|
v-for="i in 10"
|
||||||
|
:key="'food-' + i"
|
||||||
|
class="pixel-block"
|
||||||
|
:class="{
|
||||||
|
'filled': i <= Math.floor(hunger / 10),
|
||||||
|
'color-yellow': i <= Math.floor(hunger / 10)
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span class="stat-value" :class="{ 'warning': hunger < 30 }">{{ hunger }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-row">
|
||||||
|
<div class="stat-icon pixel-health"></div>
|
||||||
|
<div class="pixel-bar">
|
||||||
|
<div
|
||||||
|
v-for="i in 10"
|
||||||
|
:key="'health-' + i"
|
||||||
|
class="pixel-block"
|
||||||
|
:class="{
|
||||||
|
'filled': i <= Math.floor(health / 10),
|
||||||
|
'color-green': i <= Math.floor(health / 10)
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span class="stat-value" :class="{ 'warning': health < 30 }">{{ health }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
hunger: {
|
||||||
|
type: Number,
|
||||||
|
default: 100
|
||||||
|
},
|
||||||
|
happiness: {
|
||||||
|
type: Number,
|
||||||
|
default: 100
|
||||||
|
},
|
||||||
|
health: {
|
||||||
|
type: Number,
|
||||||
|
default: 100
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stats-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 4px 8px 3px 8px;
|
||||||
|
background: rgba(155, 188, 15, 0.08);
|
||||||
|
border-bottom: 2px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pixel Heart Icon */
|
||||||
|
.pixel-heart::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #ff4444;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow:
|
||||||
|
/* Row 1 */
|
||||||
|
-4px -4px 0 #ff4444, -2px -4px 0 #ff4444,
|
||||||
|
2px -4px 0 #ff4444, 4px -4px 0 #ff4444,
|
||||||
|
/* Row 2 */
|
||||||
|
-6px -2px 0 #ff4444, -4px -2px 0 #ff4444, -2px -2px 0 #ff4444, 0px -2px 0 #ff4444,
|
||||||
|
2px -2px 0 #ff4444, 4px -2px 0 #ff4444, 6px -2px 0 #ff4444,
|
||||||
|
/* Row 3 */
|
||||||
|
-6px 0px 0 #ff4444, -4px 0px 0 #ff4444, -2px 0px 0 #ff4444, 0px 0px 0 #ff4444,
|
||||||
|
2px 0px 0 #ff4444, 4px 0px 0 #ff4444, 6px 0px 0 #ff4444,
|
||||||
|
/* Row 4 */
|
||||||
|
-4px 2px 0 #ff4444, -2px 2px 0 #ff4444, 0px 2px 0 #ff4444,
|
||||||
|
2px 2px 0 #ff4444, 4px 2px 0 #ff4444,
|
||||||
|
/* Row 5 */
|
||||||
|
-2px 4px 0 #ff4444, 0px 4px 0 #ff4444, 2px 4px 0 #ff4444,
|
||||||
|
/* Row 6 */
|
||||||
|
0px 6px 0 #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pixel Food/Meat Icon */
|
||||||
|
.pixel-food::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #cc6633;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow:
|
||||||
|
/* Bone shape */
|
||||||
|
-4px -4px 0 #ffaa88, -2px -4px 0 #ffaa88,
|
||||||
|
-6px -2px 0 #ffaa88, -4px -2px 0 #ffaa88, -2px -2px 0 #cc6633, 0px -2px 0 #cc6633,
|
||||||
|
-4px 0px 0 #ffaa88, -2px 0px 0 #cc6633, 0px 0px 0 #cc6633,
|
||||||
|
-2px 2px 0 #cc6633, 0px 2px 0 #cc6633, 2px 2px 0 #cc6633, 4px 2px 0 #ffaa88, 6px 2px 0 #ffaa88,
|
||||||
|
-2px 4px 0 #ffaa88, 0px 4px 0 #ffaa88, 2px 4px 0 #cc6633, 4px 4px 0 #ffaa88;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pixel Health/Cross Icon */
|
||||||
|
.pixel-health::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #44cc44;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow:
|
||||||
|
/* Vertical bar */
|
||||||
|
0px -6px 0 #44cc44, 0px -4px 0 #44cc44,
|
||||||
|
/* Horizontal bar */
|
||||||
|
-6px -2px 0 #44cc44, -4px -2px 0 #44cc44, -2px -2px 0 #44cc44, 0px -2px 0 #44cc44,
|
||||||
|
2px -2px 0 #44cc44, 4px -2px 0 #44cc44, 6px -2px 0 #44cc44,
|
||||||
|
/* Vertical continue */
|
||||||
|
0px 0px 0 #44cc44, 0px 2px 0 #44cc44,
|
||||||
|
0px 4px 0 #44cc44, 0px 6px 0 #44cc44;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.pixel-bar {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
border: 2px solid #000;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pixel-block {
|
||||||
|
flex: 1;
|
||||||
|
height: 10px;
|
||||||
|
background: #ddd;
|
||||||
|
border-right: 2px solid #000;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pixel-block:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pixel-block.filled.color-red {
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
#ff6666 0%,
|
||||||
|
#ff4444 40%,
|
||||||
|
#dd2222 100%
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
0 0 4px rgba(255, 68, 68, 0.6),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.5),
|
||||||
|
inset 0 -1px 0 rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pixel-block.filled.color-yellow {
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
#ffee66 0%,
|
||||||
|
#ffdd44 40%,
|
||||||
|
#ddaa00 100%
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
0 0 4px rgba(255, 204, 0, 0.6),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.5),
|
||||||
|
inset 0 -1px 0 rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pixel-block.filled.color-green {
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
#66ee66 0%,
|
||||||
|
#44cc44 40%,
|
||||||
|
#228822 100%
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
0 0 4px rgba(68, 204, 68, 0.6),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.5),
|
||||||
|
inset 0 -1px 0 rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
width: 28px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.warning {
|
||||||
|
color: #ff0000;
|
||||||
|
animation: pulse-warning 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-warning {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
<template>
|
||||||
|
<div class="top-menu">
|
||||||
|
<button class="icon-btn icon-stats" @click="$emit('stats')" title="Stats"></button>
|
||||||
|
<button class="icon-btn icon-feed" @click="$emit('feed')" :disabled="disabled" title="Feed"></button>
|
||||||
|
<button class="icon-btn icon-play" @click="$emit('play')" :disabled="disabled" title="Play"></button>
|
||||||
|
<button class="icon-btn icon-sleep" @click="$emit('sleep')" :disabled="disabled" title="Sleep"></button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(['stats', 'feed', 'play', 'sleep']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.top-menu {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: rgba(155, 188, 15, 0.05);
|
||||||
|
border-bottom: 2px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Icon (Bar Chart) */
|
||||||
|
.icon-stats::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #333;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow:
|
||||||
|
-6px 4px 0 #333, -6px 2px 0 #333, -6px 0px 0 #333,
|
||||||
|
-2px 4px 0 #333, -2px 2px 0 #333, -2px 0px 0 #333, -2px -2px 0 #333,
|
||||||
|
2px 4px 0 #333, 2px 2px 0 #333, 2px 0px 0 #333, 2px -2px 0 #333, 2px -4px 0 #333,
|
||||||
|
6px 4px 0 #333, 6px 2px 0 #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Feed Icon (Apple/Food) */
|
||||||
|
.icon-feed::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #ff4444;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow:
|
||||||
|
-2px -6px 0 #228822,
|
||||||
|
-4px -2px 0 #ff4444, -2px -2px 0 #ff4444, 0px -2px 0 #ff4444, 2px -2px 0 #ff4444,
|
||||||
|
-4px 0px 0 #ff4444, -2px 0px 0 #ff4444, 0px 0px 0 #ff4444, 2px 0px 0 #ff4444, 4px 0px 0 #ff4444,
|
||||||
|
-4px 2px 0 #ff4444, -2px 2px 0 #ff4444, 0px 2px 0 #ff4444, 2px 2px 0 #ff4444, 4px 2px 0 #ff4444,
|
||||||
|
-2px 4px 0 #ff4444, 0px 4px 0 #ff4444, 2px 4px 0 #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Play Icon (Ball/Game) */
|
||||||
|
.icon-play::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #4444ff;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow:
|
||||||
|
-2px -4px 0 #4444ff, 0px -4px 0 #4444ff, 2px -4px 0 #4444ff,
|
||||||
|
-4px -2px 0 #4444ff, -2px -2px 0 #4444ff, 0px -2px 0 #4444ff, 2px -2px 0 #4444ff, 4px -2px 0 #4444ff,
|
||||||
|
-4px 0px 0 #4444ff, -2px 0px 0 #4444ff, 0px 0px 0 #4444ff, 2px 0px 0 #4444ff, 4px 0px 0 #4444ff,
|
||||||
|
-4px 2px 0 #4444ff, -2px 2px 0 #4444ff, 0px 2px 0 #4444ff, 2px 2px 0 #4444ff, 4px 2px 0 #4444ff,
|
||||||
|
-2px 4px 0 #4444ff, 0px 4px 0 #4444ff, 2px 4px 0 #4444ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sleep Icon (Moon) */
|
||||||
|
.icon-sleep::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #ffcc00;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow:
|
||||||
|
0px -6px 0 #ffcc00, 2px -6px 0 #ffcc00,
|
||||||
|
-2px -4px 0 #ffcc00, 0px -4px 0 #ffcc00, 2px -4px 0 #ffcc00, 4px -4px 0 #ffcc00,
|
||||||
|
-2px -2px 0 #ffcc00, 0px -2px 0 #ffcc00, 4px -2px 0 #ffcc00,
|
||||||
|
-2px 0px 0 #ffcc00, 0px 0px 0 #ffcc00, 4px 0px 0 #ffcc00,
|
||||||
|
-2px 2px 0 #ffcc00, 0px 2px 0 #ffcc00, 4px 2px 0 #ffcc00,
|
||||||
|
-2px 4px 0 #ffcc00, 0px 4px 0 #ffcc00, 2px 4px 0 #ffcc00, 4px 4px 0 #ffcc00,
|
||||||
|
0px 6px 0 #ffcc00, 2px 6px 0 #ffcc00;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||||
|
|
||||||
|
export function usePetSystem() {
|
||||||
|
// --- State ---
|
||||||
|
const stage = ref('egg'); // egg, baby, adult
|
||||||
|
const state = ref('idle'); // idle, sleep, eating, sick, dead, refuse
|
||||||
|
|
||||||
|
// --- Stats ---
|
||||||
|
const stats = ref({
|
||||||
|
hunger: 100, // 0-100 (0 = Starving)
|
||||||
|
happiness: 100, // 0-100 (0 = Depressed)
|
||||||
|
health: 100, // 0-100 (0 = Sick risk)
|
||||||
|
weight: 500, // grams
|
||||||
|
age: 0, // days
|
||||||
|
poopCount: 0 // Number of poops on screen
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Internal Timers ---
|
||||||
|
let gameLoopId = null;
|
||||||
|
const TICK_RATE = 3000; // 3 seconds per tick
|
||||||
|
|
||||||
|
const isCleaning = ref(false);
|
||||||
|
|
||||||
|
// --- Actions ---
|
||||||
|
|
||||||
|
function feed() {
|
||||||
|
if (state.value === 'sleep' || state.value === 'dead' || stage.value === 'egg' || isCleaning.value) return false;
|
||||||
|
|
||||||
|
if (state.value === 'sick' || stats.value.hunger >= 90) {
|
||||||
|
// Refuse food if sick or full
|
||||||
|
triggerState('refuse', 2000);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eat
|
||||||
|
triggerState('eating', 3000); // Animation duration
|
||||||
|
stats.value.hunger = Math.min(100, stats.value.hunger + 20);
|
||||||
|
stats.value.weight += 50;
|
||||||
|
|
||||||
|
// Chance to poop after eating
|
||||||
|
if (Math.random() < 0.3) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (stats.value.poopCount < 4) {
|
||||||
|
stats.value.poopCount++;
|
||||||
|
}
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function play() {
|
||||||
|
if (state.value !== 'idle' || stage.value === 'egg' || isCleaning.value) return false;
|
||||||
|
|
||||||
|
stats.value.happiness = Math.min(100, stats.value.happiness + 15);
|
||||||
|
stats.value.weight -= 10; // Exercise burns calories
|
||||||
|
stats.value.hunger = Math.max(0, stats.value.hunger - 5);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clean() {
|
||||||
|
if (stats.value.poopCount > 0 && !isCleaning.value) {
|
||||||
|
isCleaning.value = true;
|
||||||
|
|
||||||
|
// Delay removal for animation
|
||||||
|
setTimeout(() => {
|
||||||
|
stats.value.poopCount = 0;
|
||||||
|
stats.value.happiness += 10;
|
||||||
|
isCleaning.value = false;
|
||||||
|
}, 2000); // 2 seconds flush animation
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep() {
|
||||||
|
if (isCleaning.value) return;
|
||||||
|
|
||||||
|
if (state.value === 'idle') {
|
||||||
|
state.value = 'sleep';
|
||||||
|
} else if (state.value === 'sleep') {
|
||||||
|
state.value = 'idle'; // Wake up
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Game Loop ---
|
||||||
|
function tick() {
|
||||||
|
if (state.value === 'dead' || stage.value === 'egg') return;
|
||||||
|
|
||||||
|
// Decrease stats naturally
|
||||||
|
if (state.value !== 'sleep') {
|
||||||
|
stats.value.hunger = Math.max(0, stats.value.hunger - 2);
|
||||||
|
stats.value.happiness = Math.max(0, stats.value.happiness - 1);
|
||||||
|
} else {
|
||||||
|
// Slower decay when sleeping
|
||||||
|
stats.value.hunger = Math.max(0, stats.value.hunger - 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random poop generation (5% chance per tick)
|
||||||
|
if (state.value !== 'sleep' && Math.random() < 0.05 && stats.value.poopCount < 4 && !isCleaning.value) {
|
||||||
|
stats.value.poopCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health Logic (Poop hurts health)
|
||||||
|
if (stats.value.poopCount > 0) {
|
||||||
|
stats.value.health = Math.max(0, stats.value.health - (2 * stats.value.poopCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.value.hunger === 0) {
|
||||||
|
stats.value.health = Math.max(0, stats.value.health - 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sickness Check
|
||||||
|
if (stats.value.health < 30 && state.value !== 'sick') {
|
||||||
|
if (Math.random() < 0.3) {
|
||||||
|
state.value = 'sick';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Death Check
|
||||||
|
if (stats.value.health === 0) {
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
function triggerState(tempState, duration) {
|
||||||
|
const previousState = state.value;
|
||||||
|
state.value = tempState;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (state.value === tempState) { // Only revert if state hasn't changed again
|
||||||
|
state.value = previousState === 'sleep' ? 'idle' : 'idle';
|
||||||
|
}
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hatchEgg() {
|
||||||
|
if (stage.value === 'egg') {
|
||||||
|
stage.value = 'baby'; // or 'adult' for now since we only have that sprite
|
||||||
|
// Let's map 'baby' to our 'adult' sprite for now, or just use 'adult'
|
||||||
|
stage.value = 'adult';
|
||||||
|
state.value = 'idle';
|
||||||
|
stats.value.hunger = 50;
|
||||||
|
stats.value.happiness = 50;
|
||||||
|
stats.value.health = 100;
|
||||||
|
stats.value.poopCount = 0;
|
||||||
|
isCleaning.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
stage.value = 'egg';
|
||||||
|
state.value = 'idle';
|
||||||
|
isCleaning.value = false;
|
||||||
|
stats.value = {
|
||||||
|
hunger: 100,
|
||||||
|
happiness: 100,
|
||||||
|
health: 100,
|
||||||
|
weight: 500,
|
||||||
|
age: 0,
|
||||||
|
poopCount: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Lifecycle ---
|
||||||
|
onMounted(() => {
|
||||||
|
gameLoopId = setInterval(tick, TICK_RATE);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (gameLoopId) clearInterval(gameLoopId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
stage,
|
||||||
|
state,
|
||||||
|
stats,
|
||||||
|
isCleaning,
|
||||||
|
feed,
|
||||||
|
play,
|
||||||
|
clean,
|
||||||
|
sleep,
|
||||||
|
hatchEgg,
|
||||||
|
reset
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -203,6 +203,28 @@ export const SPRITE_PRESETS = {
|
||||||
blushPixels: [
|
blushPixels: [
|
||||||
[3, 7], [10, 7]
|
[3, 7], [10, 7]
|
||||||
],
|
],
|
||||||
|
eyePixels: [
|
||||||
|
[3, 6], [4, 6], // Left eye
|
||||||
|
[8, 6], [9, 6] // Right eye
|
||||||
|
],
|
||||||
|
spriteEyesClosed: [
|
||||||
|
'0000000000000000',
|
||||||
|
'0011000000110000',
|
||||||
|
'0124444111442100',
|
||||||
|
'0123222323221000',
|
||||||
|
'0122322223221000',
|
||||||
|
'0122522222522100',
|
||||||
|
'0122222222222100', // row 6 - Eyes closed (all '2' = closed eyes)
|
||||||
|
'0112223322221100',
|
||||||
|
'0122220222221000',
|
||||||
|
'0011222222110000',
|
||||||
|
'0001222222121000',
|
||||||
|
'0001222222121000',
|
||||||
|
'0001100110110000',
|
||||||
|
'0000000000000000',
|
||||||
|
'0000000000000000',
|
||||||
|
'0000000000000000',
|
||||||
|
],
|
||||||
iconBackLeft: { x: 2, y: 2 },
|
iconBackLeft: { x: 2, y: 2 },
|
||||||
iconBackRight: { x: 13, y: 2 },
|
iconBackRight: { x: 13, y: 2 },
|
||||||
|
|
||||||
|
|
@ -231,5 +253,4 @@ export const SPRITE_PRESETS = {
|
||||||
'3': '#ffb74d', // Orange tiger stripes
|
'3': '#ffb74d', // Orange tiger stripes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue