feat:merge health
This commit is contained in:
parent
737b74f220
commit
bae581b778
88
src/App.vue
88
src/App.vue
|
|
@ -8,7 +8,16 @@ 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 statsMode = ref('none'); // 'none', 'bars', 'details'
|
||||||
|
|
||||||
|
// Menu State
|
||||||
|
const isMenuOpen = ref(false);
|
||||||
|
const selectedMenuIndex = ref(0);
|
||||||
|
// Combined menu items: Top (0-3) + Bottom (4-7)
|
||||||
|
const MENU_ITEMS = [
|
||||||
|
'stats', 'feed', 'play', 'sleep', // Top Menu
|
||||||
|
'clean', 'medicine', 'training', 'info' // Action Menu
|
||||||
|
];
|
||||||
|
|
||||||
// Initialize Pet System
|
// Initialize Pet System
|
||||||
const {
|
const {
|
||||||
|
|
@ -24,8 +33,48 @@ const {
|
||||||
reset
|
reset
|
||||||
} = usePetSystem();
|
} = usePetSystem();
|
||||||
|
|
||||||
|
// Handle Physical Buttons
|
||||||
|
function handleButton(btnId) {
|
||||||
|
console.log('Button pressed:', btnId);
|
||||||
|
|
||||||
|
// 0. Check if PetGame wants to handle the input (e.g. Prayer Menu, Minigames)
|
||||||
|
if (petGameRef.value && petGameRef.value.handleInput) {
|
||||||
|
if (petGameRef.value.handleInput(btnId)) {
|
||||||
|
return; // Input handled by game component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Idle State (Menu Closed)
|
||||||
|
if (!isMenuOpen.value) {
|
||||||
|
if (btnId === 1) {
|
||||||
|
// Button 1: Open Menu
|
||||||
|
isMenuOpen.value = true;
|
||||||
|
selectedMenuIndex.value = 0; // Default to first item
|
||||||
|
}
|
||||||
|
// Buttons 2 & 3 do nothing in idle state (or could trigger other things)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Menu Active State
|
||||||
|
if (isMenuOpen.value) {
|
||||||
|
if (btnId === 2) {
|
||||||
|
// Button 2: Left (Previous)
|
||||||
|
selectedMenuIndex.value = (selectedMenuIndex.value - 1 + MENU_ITEMS.length) % MENU_ITEMS.length;
|
||||||
|
} else if (btnId === 3) {
|
||||||
|
// Button 3: Right (Next)
|
||||||
|
selectedMenuIndex.value = (selectedMenuIndex.value + 1) % MENU_ITEMS.length;
|
||||||
|
} else if (btnId === 1) {
|
||||||
|
// Button 1: Confirm
|
||||||
|
const action = MENU_ITEMS[selectedMenuIndex.value];
|
||||||
|
handleAction(action);
|
||||||
|
isMenuOpen.value = false; // Close menu after selection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Action Menu Events
|
// Handle Action Menu Events
|
||||||
function handleAction(action) {
|
function handleAction(action) {
|
||||||
|
console.log('Action triggered:', action);
|
||||||
switch(action) {
|
switch(action) {
|
||||||
case 'feed':
|
case 'feed':
|
||||||
const feedResult = feed();
|
const feedResult = feed();
|
||||||
|
|
@ -51,8 +100,25 @@ function handleAction(action) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'stats':
|
case 'stats':
|
||||||
// Toggle stats display
|
// Toggle stats mode: none -> bars -> details -> none
|
||||||
showStats.value = !showStats.value;
|
if (statsMode.value === 'none') statsMode.value = 'bars';
|
||||||
|
else if (statsMode.value === 'bars') statsMode.value = 'details';
|
||||||
|
else statsMode.value = 'none';
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
// Show info (same as stats details for now, or separate)
|
||||||
|
if (statsMode.value !== 'details') statsMode.value = 'details';
|
||||||
|
else statsMode.value = 'none';
|
||||||
|
break;
|
||||||
|
case 'training':
|
||||||
|
// Show Prayer Menu (handled in PetGame via prop or event?)
|
||||||
|
// We need to pass this down or handle it here.
|
||||||
|
// Currently PetGame handles 'training' event to show menu.
|
||||||
|
// We can just emit the action to PetGame if we move logic there,
|
||||||
|
// or better, expose a method on PetGame.
|
||||||
|
if (petGameRef.value) {
|
||||||
|
petGameRef.value.openPrayerMenu();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'settings':
|
case 'settings':
|
||||||
// Show reset options
|
// Show reset options
|
||||||
|
|
@ -62,16 +128,6 @@ function handleAction(action) {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'jiaobei':
|
|
||||||
// 擲筊功能 - 待實作
|
|
||||||
console.log('擲筊功能');
|
|
||||||
// TODO: 實作擲筊邏輯
|
|
||||||
break;
|
|
||||||
case 'fortune':
|
|
||||||
// 求籤功能 - 待實作
|
|
||||||
console.log('求籤功能');
|
|
||||||
// TODO: 實作求籤邏輯
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
console.log('Action not implemented:', action);
|
console.log('Action not implemented:', action);
|
||||||
}
|
}
|
||||||
|
|
@ -84,7 +140,7 @@ function setPetState(newState) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DeviceShell>
|
<DeviceShell @btn1="handleButton(1)" @btn2="handleButton(2)" @btn3="handleButton(3)">
|
||||||
<DeviceScreen>
|
<DeviceScreen>
|
||||||
<!-- Dynamic Component Switching -->
|
<!-- Dynamic Component Switching -->
|
||||||
<PetGame
|
<PetGame
|
||||||
|
|
@ -94,7 +150,9 @@ function setPetState(newState) {
|
||||||
:stage="stage"
|
:stage="stage"
|
||||||
:stats="stats"
|
:stats="stats"
|
||||||
:isCleaning="isCleaning"
|
:isCleaning="isCleaning"
|
||||||
:showStats="showStats"
|
:statsMode="statsMode"
|
||||||
|
:isMenuOpen="isMenuOpen"
|
||||||
|
:selectedMenuIndex="selectedMenuIndex"
|
||||||
@update:state="state = $event"
|
@update:state="state = $event"
|
||||||
@action="handleAction"
|
@action="handleAction"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,33 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="action-menu">
|
<div class="action-menu">
|
||||||
<button class="icon-btn icon-clean" @click="$emit('clean')" :disabled="disabled || poopCount === 0" title="清理"></button>
|
<button
|
||||||
<button class="icon-btn icon-medicine" @click="$emit('medicine')" :disabled="disabled || !isSick" title="治療"></button>
|
class="icon-btn icon-clean"
|
||||||
<button class="icon-btn icon-training" @click="$emit('training')" :disabled="disabled" title="祈禱"></button>
|
:class="{ active: isMenuOpen && selectedIndex === 4 }"
|
||||||
<button class="icon-btn icon-info" @click="$emit('info')" :disabled="disabled" title="資訊"></button>
|
@click="$emit('clean')"
|
||||||
|
:disabled="disabled || poopCount === 0"
|
||||||
|
title="清理"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
class="icon-btn icon-medicine"
|
||||||
|
:class="{ active: isMenuOpen && selectedIndex === 5 }"
|
||||||
|
@click="$emit('medicine')"
|
||||||
|
:disabled="disabled || !isSick"
|
||||||
|
title="治療"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
class="icon-btn icon-training"
|
||||||
|
:class="{ active: isMenuOpen && selectedIndex === 6 }"
|
||||||
|
@click="$emit('training')"
|
||||||
|
:disabled="disabled"
|
||||||
|
title="祈禱"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
class="icon-btn icon-info"
|
||||||
|
:class="{ active: isMenuOpen && selectedIndex === 7 }"
|
||||||
|
@click="$emit('info')"
|
||||||
|
:disabled="disabled"
|
||||||
|
title="資訊"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -24,6 +48,14 @@ const props = defineProps({
|
||||||
isSick: {
|
isSick: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
isMenuOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
selectedIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -48,10 +80,20 @@ defineEmits(['clean', 'medicine', 'training', 'info']);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
opacity: 0.5; /* Default dim */
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn.active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
filter: drop-shadow(0 0 2px rgba(0,0,0,0.5));
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-btn:disabled {
|
.icon-btn:disabled {
|
||||||
opacity: 0.3;
|
opacity: 0.2;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<div class="btn"></div>
|
<div class="btn" @click="$emit('btn1')"></div>
|
||||||
<div class="btn"></div>
|
<div class="btn" @click="$emit('btn2')"></div>
|
||||||
<div class="btn"></div>
|
<div class="btn" @click="$emit('btn3')"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isShaking && selectedStick" class="action-buttons">
|
<div v-if="!isShaking && selectedStick" class="action-buttons">
|
||||||
<button class="pixel-btn confirm-btn" @click="handleConfirm">擲筊確認</button>
|
<button class="pixel-btn confirm-btn" :class="{ active: selectedIndex === 0 }" @click="handleConfirm">擲筊確認</button>
|
||||||
<button class="pixel-btn close-btn" @click="$emit('close')">返回</button>
|
<button class="pixel-btn close-btn" :class="{ active: selectedIndex === 1 }" @click="$emit('close')">返回</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 在搖動時也顯示返回按鈕,但位置可能需要調整 -->
|
<!-- 在搖動時也顯示返回按鈕,但位置可能需要調整 -->
|
||||||
<div v-if="isShaking" class="action-buttons">
|
<div v-if="isShaking" class="action-buttons">
|
||||||
<button class="pixel-btn close-btn" @click="$emit('close')">返回</button>
|
<button class="pixel-btn close-btn" :class="{ active: selectedIndex === 0 }" @click="$emit('close')">返回</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -37,6 +37,13 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
selectedIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['complete', 'close']);
|
const emit = defineEmits(['complete', 'close']);
|
||||||
|
|
||||||
const isShaking = ref(true);
|
const isShaking = ref(true);
|
||||||
|
|
@ -68,6 +75,11 @@ function handleConfirm() {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
startShake();
|
startShake();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
hasFallen,
|
||||||
|
handleConfirm
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -220,7 +232,7 @@ onMounted(() => {
|
||||||
.status-text {
|
.status-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-shadow: 1px 1px 0 #000;
|
/* text-shadow: 1px 1px 0 #000; Removed to fix ghosting */
|
||||||
min-height: 18px;
|
min-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,7 +252,7 @@ onMounted(() => {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.1s;
|
transition: all 0.1s;
|
||||||
text-shadow: 1px 1px 0 #000;
|
/* text-shadow: 1px 1px 0 #000; Removed to fix ghosting */
|
||||||
box-shadow: 2px 2px 0 rgba(0,0,0,0.5);
|
box-shadow: 2px 2px 0 rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,6 +266,14 @@ onMounted(() => {
|
||||||
box-shadow: 1px 1px 0 rgba(0,0,0,0.5);
|
box-shadow: 1px 1px 0 rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pixel-btn.active {
|
||||||
|
transform: scale(1.1);
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
border-color: #000;
|
||||||
|
box-shadow: 0 0 8px rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
.confirm-btn {
|
.confirm-btn {
|
||||||
border-color: #ffcc00;
|
border-color: #ffcc00;
|
||||||
color: #ffcc00;
|
color: #ffcc00;
|
||||||
|
|
|
||||||
|
|
@ -30,19 +30,19 @@
|
||||||
<div v-if="!isTossing" class="action-buttons">
|
<div v-if="!isTossing" class="action-buttons">
|
||||||
<!-- 一般模式 -->
|
<!-- 一般模式 -->
|
||||||
<template v-if="mode === 'normal'">
|
<template v-if="mode === 'normal'">
|
||||||
<button class="pixel-btn retry-btn" @click="startToss">再一次</button>
|
<button class="pixel-btn retry-btn" :class="{ active: selectedIndex === 0 }" @click="startToss">再一次</button>
|
||||||
<button class="pixel-btn close-btn" @click="handleClose">關閉</button>
|
<button class="pixel-btn close-btn" :class="{ active: selectedIndex === 1 }" @click="handleClose">關閉</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 求籤模式 -->
|
<!-- 求籤模式 -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- 失敗 (非聖筊) -->
|
<!-- 失敗 (非聖筊) -->
|
||||||
<button v-if="resultType !== 'saint'" class="pixel-btn close-btn" @click="handleRetryFortune">重新求籤</button>
|
<button v-if="resultType !== 'saint'" class="pixel-btn close-btn" :class="{ active: selectedIndex === 0 }" @click="handleRetryFortune">重新求籤</button>
|
||||||
|
|
||||||
<!-- 成功 (聖筊) -->
|
<!-- 成功 (聖筊) -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button v-if="consecutiveCount < 2" class="pixel-btn retry-btn" @click="startToss">繼續擲筊</button>
|
<button v-if="consecutiveCount < 2" class="pixel-btn retry-btn" :class="{ active: selectedIndex === 0 }" @click="startToss">繼續擲筊</button>
|
||||||
<button v-else class="pixel-btn retry-btn" @click="$emit('finish-fortune')">查看籤詩</button>
|
<button v-else class="pixel-btn retry-btn" :class="{ active: selectedIndex === 0 }" @click="$emit('finish-fortune')">查看籤詩</button>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -61,6 +61,10 @@ const props = defineProps({
|
||||||
consecutiveCount: {
|
consecutiveCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
|
},
|
||||||
|
selectedIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -125,6 +129,10 @@ function handleRetryFortune() {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
startToss();
|
startToss();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
startToss
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -264,7 +272,7 @@ onMounted(() => {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
text-shadow: 1px 1px 0 #000;
|
/* text-shadow: 1px 1px 0 #000; Removed to fix ghosting */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -302,7 +310,7 @@ onMounted(() => {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.1s;
|
transition: all 0.1s;
|
||||||
text-shadow: 1px 1px 0 #000;
|
/* text-shadow: 1px 1px 0 #000; Removed to fix ghosting */
|
||||||
box-shadow: 2px 2px 0 rgba(0,0,0,0.5);
|
box-shadow: 2px 2px 0 rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,6 +324,14 @@ onMounted(() => {
|
||||||
box-shadow: 1px 1px 0 rgba(0,0,0,0.5);
|
box-shadow: 1px 1px 0 rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pixel-btn.active {
|
||||||
|
transform: scale(1.1);
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
border-color: #000;
|
||||||
|
box-shadow: 0 0 8px rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
.retry-btn {
|
.retry-btn {
|
||||||
border-color: #ffcc00;
|
border-color: #ffcc00;
|
||||||
color: #ffcc00;
|
color: #ffcc00;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
<!-- Top Menu -->
|
<!-- Top Menu -->
|
||||||
<TopMenu
|
<TopMenu
|
||||||
:disabled="stage === 'egg'"
|
:disabled="stage === 'egg'"
|
||||||
|
:isMenuOpen="isMenuOpen"
|
||||||
|
:selectedIndex="selectedMenuIndex"
|
||||||
@stats="$emit('action', 'stats')"
|
@stats="$emit('action', 'stats')"
|
||||||
@feed="$emit('action', 'feed')"
|
@feed="$emit('action', 'feed')"
|
||||||
@play="$emit('action', 'play')"
|
@play="$emit('action', 'play')"
|
||||||
|
|
@ -11,12 +13,18 @@
|
||||||
|
|
||||||
<!-- Stats Dashboard (Toggelable) -->
|
<!-- Stats Dashboard (Toggelable) -->
|
||||||
<StatsBar
|
<StatsBar
|
||||||
v-if="showStats"
|
v-if="statsMode === 'bars'"
|
||||||
:hunger="stats?.hunger || 100"
|
:hunger="stats?.hunger || 100"
|
||||||
:happiness="stats?.happiness || 100"
|
:happiness="stats?.happiness || 100"
|
||||||
:health="stats?.health || 100"
|
:health="stats?.health || 100"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Detailed Info Overlay -->
|
||||||
|
<PetInfo
|
||||||
|
v-if="statsMode === 'details'"
|
||||||
|
:stats="stats"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Game Area (Center) -->
|
<!-- Game Area (Center) -->
|
||||||
<div class="pet-game-container" ref="containerRef">
|
<div class="pet-game-container" ref="containerRef">
|
||||||
<!-- 關燈黑色遮罩 -->
|
<!-- 關燈黑色遮罩 -->
|
||||||
|
|
@ -133,6 +141,7 @@
|
||||||
<!-- Prayer Menu (覆蓋整個遊戲區域) -->
|
<!-- Prayer Menu (覆蓋整個遊戲區域) -->
|
||||||
<PrayerMenu
|
<PrayerMenu
|
||||||
v-if="showPrayerMenu"
|
v-if="showPrayerMenu"
|
||||||
|
:selectedIndex="prayerMenuIndex"
|
||||||
@select="handlePrayerSelect"
|
@select="handlePrayerSelect"
|
||||||
@close="showPrayerMenu = false"
|
@close="showPrayerMenu = false"
|
||||||
/>
|
/>
|
||||||
|
|
@ -140,8 +149,10 @@
|
||||||
<!-- Jiaobei Animation (覆蓋遊戲區域) -->
|
<!-- Jiaobei Animation (覆蓋遊戲區域) -->
|
||||||
<JiaobeiAnimation
|
<JiaobeiAnimation
|
||||||
v-if="showJiaobeiAnimation"
|
v-if="showJiaobeiAnimation"
|
||||||
|
ref="jiaobeiRef"
|
||||||
:mode="fortuneMode"
|
:mode="fortuneMode"
|
||||||
:consecutiveCount="consecutiveSaintCount"
|
:consecutiveCount="consecutiveSaintCount"
|
||||||
|
:selectedIndex="jiaobeiIndex"
|
||||||
@close="handleJiaobeiClose"
|
@close="handleJiaobeiClose"
|
||||||
@result="handleJiaobeiResult"
|
@result="handleJiaobeiResult"
|
||||||
@retry-fortune="handleRetryFortune"
|
@retry-fortune="handleRetryFortune"
|
||||||
|
|
@ -151,6 +162,8 @@
|
||||||
<!-- Fortune Stick Animation -->
|
<!-- Fortune Stick Animation -->
|
||||||
<FortuneStickAnimation
|
<FortuneStickAnimation
|
||||||
v-if="showFortuneStick"
|
v-if="showFortuneStick"
|
||||||
|
ref="fortuneStickRef"
|
||||||
|
:selectedIndex="fortuneStickIndex"
|
||||||
@complete="handleStickComplete"
|
@complete="handleStickComplete"
|
||||||
@close="handleFortuneStickClose"
|
@close="handleFortuneStickClose"
|
||||||
/>
|
/>
|
||||||
|
|
@ -168,6 +181,8 @@
|
||||||
:poopCount="stats?.poopCount || 0"
|
:poopCount="stats?.poopCount || 0"
|
||||||
:health="stats?.health || 100"
|
:health="stats?.health || 100"
|
||||||
:isSick="state === 'sick'"
|
:isSick="state === 'sick'"
|
||||||
|
:isMenuOpen="isMenuOpen"
|
||||||
|
:selectedIndex="selectedMenuIndex"
|
||||||
@clean="$emit('action', 'clean')"
|
@clean="$emit('action', 'clean')"
|
||||||
@medicine="$emit('action', 'medicine')"
|
@medicine="$emit('action', 'medicine')"
|
||||||
@training="showPrayerMenu = true"
|
@training="showPrayerMenu = true"
|
||||||
|
|
@ -182,6 +197,7 @@ 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 StatsBar from './StatsBar.vue';
|
||||||
|
import PetInfo from './PetInfo.vue';
|
||||||
import ActionMenu from './ActionMenu.vue';
|
import ActionMenu from './ActionMenu.vue';
|
||||||
import TopMenu from './TopMenu.vue';
|
import TopMenu from './TopMenu.vue';
|
||||||
import PrayerMenu from './PrayerMenu.vue';
|
import PrayerMenu from './PrayerMenu.vue';
|
||||||
|
|
@ -203,13 +219,21 @@ const props = defineProps({
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({ hunger: 100, happiness: 100, health: 100, poopCount: 0 })
|
default: () => ({ hunger: 100, happiness: 100, health: 100, poopCount: 0 })
|
||||||
},
|
},
|
||||||
showStats: {
|
statsMode: {
|
||||||
type: Boolean,
|
type: String,
|
||||||
default: false
|
default: 'none' // 'none', 'bars', 'details'
|
||||||
},
|
},
|
||||||
isCleaning: {
|
isCleaning: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
isMenuOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
selectedMenuIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -264,16 +288,7 @@ function handleFortuneStickClose() {
|
||||||
consecutiveSaintCount.value = 0;
|
consecutiveSaintCount.value = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleJiaobeiResult(type) {
|
|
||||||
if (fortuneMode.value === 'fortune') {
|
|
||||||
if (type === 'saint') {
|
|
||||||
consecutiveSaintCount.value++;
|
|
||||||
} else {
|
|
||||||
// 如果不是聖筊,計數歸零(邏輯上失敗了,需要重新求籤)
|
|
||||||
consecutiveSaintCount.value = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRetryFortune() {
|
function handleRetryFortune() {
|
||||||
// 重新開始搖籤
|
// 重新開始搖籤
|
||||||
|
|
@ -379,11 +394,38 @@ const FOOD_PALETTE = FOOD_OPTIONS[currentFood].palette;
|
||||||
|
|
||||||
|
|
||||||
const CURRENT_PRESET = SPRITE_PRESETS.tinyTigerCatB;
|
const CURRENT_PRESET = SPRITE_PRESETS.tinyTigerCatB;
|
||||||
const pixelSize = CURRENT_PRESET.pixelSize;
|
// Use meta.pixelSize if available, fallback to root pixelSize (for backward compatibility)
|
||||||
|
const pixelSize = CURRENT_PRESET.appearance?.pixelSize || CURRENT_PRESET.pixelSize || 3;
|
||||||
|
|
||||||
// Define dimensions
|
// Helper to get current stage config
|
||||||
const rows = CURRENT_PRESET.sprite.length;
|
const currentStageConfig = computed(() => {
|
||||||
const cols = CURRENT_PRESET.sprite[0].length;
|
if (!CURRENT_PRESET.lifecycle) return null;
|
||||||
|
return CURRENT_PRESET.lifecycle.stages.find(s => s.id === props.stage);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper to get sprite key for current stage
|
||||||
|
const currentSpriteKey = computed(() => {
|
||||||
|
if (currentStageConfig.value) {
|
||||||
|
return currentStageConfig.value.spriteKey;
|
||||||
|
}
|
||||||
|
return props.stage === 'egg' ? 'egg' : 'child'; // Fallback
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define dimensions based on the first available sprite
|
||||||
|
// We assume all sprites have the same dimensions for now
|
||||||
|
const getBaseSprite = () => {
|
||||||
|
if (CURRENT_PRESET.appearance?.sprites) {
|
||||||
|
// Try to find a valid sprite to measure
|
||||||
|
const sprites = CURRENT_PRESET.appearance.sprites;
|
||||||
|
const firstKey = Object.keys(sprites)[0];
|
||||||
|
if (firstKey && sprites[firstKey].idle) return sprites[firstKey].idle;
|
||||||
|
}
|
||||||
|
return CURRENT_PRESET.sprite; // Fallback to old structure
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseSprite = getBaseSprite();
|
||||||
|
const rows = baseSprite.length;
|
||||||
|
const cols = baseSprite[0].length;
|
||||||
const width = cols * pixelSize;
|
const width = cols * pixelSize;
|
||||||
const height = rows * pixelSize;
|
const height = rows * pixelSize;
|
||||||
|
|
||||||
|
|
@ -391,7 +433,9 @@ const height = rows * pixelSize;
|
||||||
// 2. Generate Pixels Helper
|
// 2. Generate Pixels Helper
|
||||||
function generatePixels(spriteMap, paletteOverride = null) {
|
function generatePixels(spriteMap, paletteOverride = null) {
|
||||||
const pxs = [];
|
const pxs = [];
|
||||||
const palette = paletteOverride || CURRENT_PRESET.palette;
|
// Handle new palette structure (appearance.palettes.default) vs old (palette)
|
||||||
|
const defaultPalette = CURRENT_PRESET.appearance?.palettes?.default || CURRENT_PRESET.palette;
|
||||||
|
const palette = paletteOverride || defaultPalette;
|
||||||
|
|
||||||
spriteMap.forEach((rowStr, y) => {
|
spriteMap.forEach((rowStr, y) => {
|
||||||
[...rowStr].forEach((ch, x) => {
|
[...rowStr].forEach((ch, x) => {
|
||||||
|
|
@ -400,11 +444,14 @@ function generatePixels(spriteMap, paletteOverride = null) {
|
||||||
// Only apply body part classes if NOT an egg
|
// Only apply body part classes if NOT an egg
|
||||||
let className = '';
|
let className = '';
|
||||||
if (props.stage !== 'egg') {
|
if (props.stage !== 'egg') {
|
||||||
const isTail = CURRENT_PRESET.tailPixels?.some(([tx, ty]) => tx === x && ty === y);
|
// Handle new bodyParts structure vs old root properties
|
||||||
const isLegFront = CURRENT_PRESET.legFrontPixels?.some(([lx, ly]) => lx === x && ly === y);
|
const bodyParts = CURRENT_PRESET.appearance?.bodyParts || CURRENT_PRESET;
|
||||||
const isLegBack = CURRENT_PRESET.legBackPixels?.some(([lx, ly]) => lx === x && ly === y);
|
|
||||||
const isEar = CURRENT_PRESET.earPixels?.some(([ex, ey]) => ex === x && ey === y);
|
const isTail = bodyParts.tailPixels?.some(([tx, ty]) => tx === x && ty === y);
|
||||||
const isBlush = CURRENT_PRESET.blushPixels?.some(([bx, by]) => bx === x && by === y);
|
const isLegFront = bodyParts.legFrontPixels?.some(([lx, ly]) => lx === x && ly === y);
|
||||||
|
const isLegBack = bodyParts.legBackPixels?.some(([lx, ly]) => lx === x && ly === y);
|
||||||
|
const isEar = bodyParts.earPixels?.some(([ex, ey]) => ex === x && ey === y);
|
||||||
|
const isBlush = bodyParts.blushPixels?.some(([bx, by]) => bx === x && by === y);
|
||||||
|
|
||||||
if (isTail) className += ' tail-pixel';
|
if (isTail) className += ' tail-pixel';
|
||||||
if (isLegFront) className += ' leg-front';
|
if (isLegFront) className += ' leg-front';
|
||||||
|
|
@ -447,12 +494,33 @@ const foodVisible = ref(false);
|
||||||
const isBlinking = ref(false);
|
const isBlinking = ref(false);
|
||||||
|
|
||||||
const currentPixels = computed(() => {
|
const currentPixels = computed(() => {
|
||||||
// Priority: Egg > Blink > Mouth Open > Normal
|
// New Structure Logic
|
||||||
|
if (CURRENT_PRESET.appearance?.sprites) {
|
||||||
|
const sprites = CURRENT_PRESET.appearance.sprites;
|
||||||
|
const key = currentSpriteKey.value;
|
||||||
|
const stageSprites = sprites[key];
|
||||||
|
|
||||||
|
if (!stageSprites) return []; // Should not happen if config is correct
|
||||||
|
|
||||||
|
// Priority: Blink > Mouth Open > Normal
|
||||||
|
// Note: Egg stage is handled by currentSpriteKey returning 'egg' which maps to egg sprites
|
||||||
|
|
||||||
|
if (isBlinking.value && stageSprites.eyesClosed) {
|
||||||
|
return generatePixels(stageSprites.eyesClosed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMouthOpen.value && stageSprites.mouthOpen) {
|
||||||
|
return generatePixels(stageSprites.mouthOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
return generatePixels(stageSprites.idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to Old Structure Logic
|
||||||
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) {
|
if (isBlinking.value && CURRENT_PRESET.spriteEyesClosed) {
|
||||||
return generatePixels(CURRENT_PRESET.spriteEyesClosed);
|
return generatePixels(CURRENT_PRESET.spriteEyesClosed);
|
||||||
}
|
}
|
||||||
|
|
@ -484,7 +552,11 @@ const iconStyle = computed(() => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function updateHeadIconsPosition() {
|
function updateHeadIconsPosition() {
|
||||||
const { iconBackLeft, iconBackRight } = CURRENT_PRESET;
|
const bodyParts = CURRENT_PRESET.appearance?.bodyParts || CURRENT_PRESET;
|
||||||
|
const { iconBackLeft, iconBackRight } = bodyParts;
|
||||||
|
|
||||||
|
if (!iconBackLeft || !iconBackRight) return;
|
||||||
|
|
||||||
const marker = isFacingRight.value ? iconBackLeft : iconBackRight;
|
const marker = isFacingRight.value ? iconBackLeft : iconBackRight;
|
||||||
const baseX = petX.value + marker.x * pixelSize;
|
const baseX = petX.value + marker.x * pixelSize;
|
||||||
const baseY = petY.value + marker.y * pixelSize;
|
const baseY = petY.value + marker.y * pixelSize;
|
||||||
|
|
@ -786,9 +858,129 @@ async function shakeHead() {
|
||||||
isShakingHead.value = false;
|
isShakingHead.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose shakeHead function to parent component
|
// Menu Indices
|
||||||
|
const prayerMenuIndex = ref(0);
|
||||||
|
const jiaobeiIndex = ref(0);
|
||||||
|
const fortuneStickIndex = ref(0);
|
||||||
|
|
||||||
|
// Refs for child components
|
||||||
|
const jiaobeiRef = ref(null);
|
||||||
|
const fortuneStickRef = ref(null);
|
||||||
|
const lastJiaobeiResult = ref(''); // Track result for button logic
|
||||||
|
|
||||||
|
// Handle Physical Button Input
|
||||||
|
function handleInput(btnId) {
|
||||||
|
// 1. Fortune Result (Highest Priority)
|
||||||
|
if (showFortuneResult.value && currentLotData.value) {
|
||||||
|
if (btnId === 1) {
|
||||||
|
handleCloseResult();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Jiaobei Animation
|
||||||
|
if (showJiaobeiAnimation.value) {
|
||||||
|
let numOptions = 1;
|
||||||
|
if (fortuneMode.value === 'normal') {
|
||||||
|
numOptions = 2; // Retry, Close
|
||||||
|
} else {
|
||||||
|
numOptions = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnId === 2) {
|
||||||
|
jiaobeiIndex.value = (jiaobeiIndex.value - 1 + numOptions) % numOptions;
|
||||||
|
} else if (btnId === 3) {
|
||||||
|
jiaobeiIndex.value = (jiaobeiIndex.value + 1) % numOptions;
|
||||||
|
} else if (btnId === 1) {
|
||||||
|
if (fortuneMode.value === 'normal') {
|
||||||
|
if (jiaobeiIndex.value === 0) {
|
||||||
|
jiaobeiRef.value?.startToss();
|
||||||
|
} else {
|
||||||
|
handleJiaobeiClose();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (lastJiaobeiResult.value !== 'saint') {
|
||||||
|
handleRetryFortune();
|
||||||
|
} else {
|
||||||
|
if (consecutiveSaintCount.value < 3) {
|
||||||
|
jiaobeiRef.value?.startToss();
|
||||||
|
} else {
|
||||||
|
handleFinishFortune();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fortune Stick Animation
|
||||||
|
if (showFortuneStick.value) {
|
||||||
|
const isResult = fortuneStickRef.value?.hasFallen;
|
||||||
|
const numOptions = isResult ? 2 : 1;
|
||||||
|
|
||||||
|
if (btnId === 2) {
|
||||||
|
fortuneStickIndex.value = (fortuneStickIndex.value - 1 + numOptions) % numOptions;
|
||||||
|
} else if (btnId === 3) {
|
||||||
|
fortuneStickIndex.value = (fortuneStickIndex.value + 1) % numOptions;
|
||||||
|
} else if (btnId === 1) {
|
||||||
|
if (isResult) {
|
||||||
|
if (fortuneStickIndex.value === 0) {
|
||||||
|
fortuneStickRef.value?.handleConfirm();
|
||||||
|
} else {
|
||||||
|
handleFortuneStickClose();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleFortuneStickClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Prayer Menu
|
||||||
|
if (showPrayerMenu.value) {
|
||||||
|
if (btnId === 2) {
|
||||||
|
prayerMenuIndex.value = (prayerMenuIndex.value - 1 + 3) % 3;
|
||||||
|
} else if (btnId === 3) {
|
||||||
|
prayerMenuIndex.value = (prayerMenuIndex.value + 1) % 3;
|
||||||
|
} else if (btnId === 1) {
|
||||||
|
if (prayerMenuIndex.value === 0) handlePrayerSelect('jiaobei');
|
||||||
|
else if (prayerMenuIndex.value === 1) handlePrayerSelect('fortune');
|
||||||
|
else showPrayerMenu.value = false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Pet Info Overlay
|
||||||
|
if (props.statsMode === 'details') {
|
||||||
|
if (btnId === 1) {
|
||||||
|
emit('action', 'stats'); // Toggle off
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleJiaobeiResult(result) {
|
||||||
|
lastJiaobeiResult.value = result;
|
||||||
|
if (result === 'saint' && fortuneMode.value === 'fortune') {
|
||||||
|
consecutiveSaintCount.value++;
|
||||||
|
} else if (fortuneMode.value === 'fortune') {
|
||||||
|
consecutiveSaintCount.value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open Prayer Menu
|
||||||
|
function openPrayerMenu() {
|
||||||
|
showPrayerMenu.value = true;
|
||||||
|
prayerMenuIndex.value = 0; // Reset index
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose functions to parent component
|
||||||
defineExpose({
|
defineExpose({
|
||||||
shakeHead
|
shakeHead,
|
||||||
|
openPrayerMenu,
|
||||||
|
handleInput
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -798,6 +990,7 @@ defineExpose({
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pet-game-container {
|
.pet-game-container {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
<template>
|
||||||
|
<div class="pet-info-overlay">
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="header">
|
||||||
|
<span class="name">{{ name }}</span>
|
||||||
|
<span class="species">{{ species }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="description">
|
||||||
|
{{ description }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="label">HP</span>
|
||||||
|
<span class="value">{{ Math.round(stats.health) }}/100</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="label">ATK</span>
|
||||||
|
<span class="value">{{ baseStats.attack }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="label">DEF</span>
|
||||||
|
<span class="value">{{ baseStats.defense }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="label">SPD</span>
|
||||||
|
<span class="value">{{ baseStats.speed }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="age-info">
|
||||||
|
Age: {{ Math.floor(stats.ageMinutes / 60) }}h {{ Math.floor(stats.ageMinutes % 60) }}m
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { SPRITE_PRESETS } from '../data/petPresets.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
stats: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// For now, we assume tinyTigerCatB. In a real app, this might be a prop.
|
||||||
|
const CONFIG = SPRITE_PRESETS.tinyTigerCatB;
|
||||||
|
|
||||||
|
const name = computed(() => CONFIG.meta.name);
|
||||||
|
const species = computed(() => CONFIG.meta.displayNameEn);
|
||||||
|
const description = computed(() => CONFIG.meta.description);
|
||||||
|
|
||||||
|
const baseStats = computed(() => {
|
||||||
|
// Calculate current stats based on stage modifiers if needed
|
||||||
|
// For now just showing base stats from config
|
||||||
|
return CONFIG.stats.base;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pet-info-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 100;
|
||||||
|
pointer-events: none; /* Let clicks pass through if needed, but usually overlay blocks */
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 2px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 280px;
|
||||||
|
font-family: monospace;
|
||||||
|
pointer-events: auto;
|
||||||
|
box-shadow: 4px 4px 0 rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
border-bottom: 2px solid #eee;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.species {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #666;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.age-info {
|
||||||
|
margin-top: 8px;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
<!-- 擲筊選項 -->
|
<!-- 擲筊選項 -->
|
||||||
<button
|
<button
|
||||||
class="prayer-option"
|
class="prayer-option"
|
||||||
|
:class="{ active: selectedIndex === 0 }"
|
||||||
@click="$emit('select', 'jiaobei')"
|
@click="$emit('select', 'jiaobei')"
|
||||||
>
|
>
|
||||||
<div class="option-icon icon-jiaobei"></div>
|
<div class="option-icon icon-jiaobei"></div>
|
||||||
|
|
@ -15,6 +16,7 @@
|
||||||
<!-- 求籤選項 -->
|
<!-- 求籤選項 -->
|
||||||
<button
|
<button
|
||||||
class="prayer-option"
|
class="prayer-option"
|
||||||
|
:class="{ active: selectedIndex === 1 }"
|
||||||
@click="$emit('select', 'fortune')"
|
@click="$emit('select', 'fortune')"
|
||||||
>
|
>
|
||||||
<div class="option-icon icon-fortune"></div>
|
<div class="option-icon icon-fortune"></div>
|
||||||
|
|
@ -23,13 +25,24 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 返回按鈕 -->
|
<!-- 返回按鈕 -->
|
||||||
<button class="back-button" @click="$emit('close')">
|
<button
|
||||||
|
class="back-button"
|
||||||
|
:class="{ active: selectedIndex === 2 }"
|
||||||
|
@click="$emit('close')"
|
||||||
|
>
|
||||||
返回
|
返回
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
selectedIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
defineEmits(['select', 'close']);
|
defineEmits(['select', 'close']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -79,77 +92,15 @@ defineEmits(['select', 'close']);
|
||||||
min-width: 60px; /* 縮小最小寬度 */
|
min-width: 60px; /* 縮小最小寬度 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.prayer-option:hover {
|
.prayer-option.active {
|
||||||
transform: translateY(-2px);
|
background: #000;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
color: #fff;
|
||||||
|
border-color: #fff;
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prayer-option:active {
|
.prayer-option.active .option-label {
|
||||||
transform: translateY(0);
|
color: #fff;
|
||||||
}
|
|
||||||
|
|
||||||
.option-icon {
|
|
||||||
width: 24px; /* 縮小圖標 */
|
|
||||||
height: 24px;
|
|
||||||
position: relative;
|
|
||||||
transform: scale(0.8); /* 稍微縮小圖標內容 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-label {
|
|
||||||
font-size: 10px; /* 縮小標籤 */
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 擲筊圖標 - 可愛版(一對圓潤的紅筊) */
|
|
||||||
.icon-jiaobei::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 2px;
|
|
||||||
height: 2px;
|
|
||||||
background: transparent;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
box-shadow:
|
|
||||||
/* --- 左邊筊杯 (胖胖的月牙) --- */
|
|
||||||
-7px -4px 0 #ff5252, -5px -4px 0 #ff5252, -3px -4px 0 #ff5252,
|
|
||||||
-9px -2px 0 #ff5252, -7px -2px 0 #ff8a80, -5px -2px 0 #ff5252, -3px -2px 0 #ff5252, -1px -2px 0 #ff5252, /* #ff8a80 是高光 */
|
|
||||||
-9px 0px 0 #ff5252, -7px 0px 0 #ff5252, -5px 0px 0 #ff5252, -3px 0px 0 #ff5252, -1px 0px 0 #ff5252,
|
|
||||||
-7px 2px 0 #ff5252, -5px 2px 0 #ff5252, -3px 2px 0 #ff5252,
|
|
||||||
-5px 4px 0 #d32f2f, /* 陰影 */
|
|
||||||
|
|
||||||
/* --- 右邊筊杯 (對稱的胖月牙) --- */
|
|
||||||
3px -4px 0 #ff5252, 5px -4px 0 #ff5252, 7px -4px 0 #ff5252,
|
|
||||||
1px -2px 0 #ff5252, 3px -2px 0 #ff5252, 5px -2px 0 #ff5252, 7px -2px 0 #ff8a80, 9px -2px 0 #ff5252, /* 高光在右側 */
|
|
||||||
1px 0px 0 #ff5252, 3px 0px 0 #ff5252, 5px 0px 0 #ff5252, 7px 0px 0 #ff5252, 9px 0px 0 #ff5252,
|
|
||||||
3px 2px 0 #ff5252, 5px 2px 0 #ff5252, 7px 2px 0 #ff5252,
|
|
||||||
5px 4px 0 #d32f2f; /* 陰影 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 求籤圖標 - 籤筒和籤條 */
|
|
||||||
.icon-fortune::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 2px;
|
|
||||||
height: 2px;
|
|
||||||
background: #8B4513; /* 木色 */
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
box-shadow:
|
|
||||||
/* 籤筒本體 */
|
|
||||||
-4px -4px 0 #8B4513, -2px -4px 0 #8B4513, 0px -4px 0 #8B4513, 2px -4px 0 #8B4513, 4px -4px 0 #8B4513,
|
|
||||||
-4px -2px 0 #8B4513, -2px -2px 0 #8B4513, 0px -2px 0 #8B4513, 2px -2px 0 #8B4513, 4px -2px 0 #8B4513,
|
|
||||||
-4px 0px 0 #8B4513, -2px 0px 0 #8B4513, 0px 0px 0 #8B4513, 2px 0px 0 #8B4513, 4px 0px 0 #8B4513,
|
|
||||||
-4px 2px 0 #8B4513, -2px 2px 0 #8B4513, 0px 2px 0 #8B4513, 2px 2px 0 #8B4513, 4px 2px 0 #8B4513,
|
|
||||||
-4px 4px 0 #8B4513, -2px 4px 0 #8B4513, 0px 4px 0 #8B4513, 2px 4px 0 #8B4513, 4px 4px 0 #8B4513,
|
|
||||||
|
|
||||||
/* 突出的籤條(紅色) */
|
|
||||||
-2px -8px 0 #d4522e, 0px -8px 0 #d4522e,
|
|
||||||
-2px -6px 0 #d4522e, 0px -6px 0 #d4522e,
|
|
||||||
2px -6px 0 #d4522e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-button {
|
.back-button {
|
||||||
|
|
@ -164,11 +115,86 @@ defineEmits(['select', 'close']);
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-button:hover {
|
.back-button.active {
|
||||||
background: rgba(255, 255, 255, 1);
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-button:active {
|
/* --- Icons (Pixel Art) --- */
|
||||||
transform: translateY(1px);
|
.option-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
/* Jiaobei (Moon Blocks) - Two red crescents */
|
||||||
|
.icon-jiaobei::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #d4522e;
|
||||||
|
transform: translate(-8px, -2px); /* Left block */
|
||||||
|
box-shadow:
|
||||||
|
2px -2px 0 #d4522e, 4px -2px 0 #d4522e,
|
||||||
|
0px 0px 0 #d4522e, 2px 0px 0 #d4522e, 4px 0px 0 #d4522e, 6px 0px 0 #d4522e,
|
||||||
|
0px 2px 0 #d4522e, 2px 2px 0 #d4522e, 4px 2px 0 #d4522e,
|
||||||
|
2px 4px 0 #d4522e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-jiaobei::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #d4522e;
|
||||||
|
transform: translate(2px, -2px) scaleX(-1); /* Right block (mirrored) */
|
||||||
|
box-shadow:
|
||||||
|
2px -2px 0 #d4522e, 4px -2px 0 #d4522e,
|
||||||
|
0px 0px 0 #d4522e, 2px 0px 0 #d4522e, 4px 0px 0 #d4522e, 6px 0px 0 #d4522e,
|
||||||
|
0px 2px 0 #d4522e, 2px 2px 0 #d4522e, 4px 2px 0 #d4522e,
|
||||||
|
2px 4px 0 #d4522e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fortune (Stick Container) */
|
||||||
|
.icon-fortune::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2px;
|
||||||
|
left: 50%;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #8b4513;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
box-shadow:
|
||||||
|
/* Container Body */
|
||||||
|
-4px 0 0 #8b4513, -2px 0 0 #a0522d, 0 0 0 #a0522d, 2px 0 0 #a0522d, 4px 0 0 #8b4513,
|
||||||
|
-4px -2px 0 #8b4513, -2px -2px 0 #a0522d, 0 -2px 0 #a0522d, 2px -2px 0 #a0522d, 4px -2px 0 #8b4513,
|
||||||
|
-4px -4px 0 #8b4513, -2px -4px 0 #a0522d, 0 -4px 0 #a0522d, 2px -4px 0 #a0522d, 4px -4px 0 #8b4513,
|
||||||
|
-4px -6px 0 #8b4513, -2px -6px 0 #a0522d, 0 -6px 0 #a0522d, 2px -6px 0 #a0522d, 4px -6px 0 #8b4513,
|
||||||
|
-4px -8px 0 #8b4513, -2px -8px 0 #a0522d, 0 -8px 0 #a0522d, 2px -8px 0 #a0522d, 4px -8px 0 #8b4513,
|
||||||
|
/* Rim */
|
||||||
|
-6px -10px 0 #5c2e0e, -4px -10px 0 #8b4513, -2px -10px 0 #8b4513, 0 -10px 0 #8b4513, 2px -10px 0 #8b4513, 4px -10px 0 #8b4513, 6px -10px 0 #5c2e0e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-fortune::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 14px;
|
||||||
|
left: 50%;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #f0d09c;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
box-shadow:
|
||||||
|
/* Sticks */
|
||||||
|
-2px 0 0 #f0d09c, 2px 0 0 #f0d09c,
|
||||||
|
-3px -2px 0 #f0d09c, 0px -2px 0 #f0d09c, 3px -2px 0 #f0d09c,
|
||||||
|
-1px -4px 0 #ff4444, 1px -4px 0 #ff4444; /* Red tips */
|
||||||
|
}</style>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,32 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="top-menu">
|
<div class="top-menu">
|
||||||
<button class="icon-btn icon-stats" @click="$emit('stats')" title="Stats"></button>
|
<button
|
||||||
<button class="icon-btn icon-feed" @click="$emit('feed')" :disabled="disabled" title="Feed"></button>
|
class="icon-btn icon-stats"
|
||||||
<button class="icon-btn icon-play" @click="$emit('play')" :disabled="disabled" title="Play"></button>
|
:class="{ active: isMenuOpen && selectedIndex === 0 }"
|
||||||
<button class="icon-btn icon-sleep" @click="$emit('sleep')" :disabled="disabled" title="Sleep"></button>
|
@click="$emit('stats')"
|
||||||
|
title="Stats"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
class="icon-btn icon-feed"
|
||||||
|
:class="{ active: isMenuOpen && selectedIndex === 1 }"
|
||||||
|
@click="$emit('feed')"
|
||||||
|
:disabled="disabled"
|
||||||
|
title="Feed"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
class="icon-btn icon-play"
|
||||||
|
:class="{ active: isMenuOpen && selectedIndex === 2 }"
|
||||||
|
@click="$emit('play')"
|
||||||
|
:disabled="disabled"
|
||||||
|
title="Play"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
class="icon-btn icon-sleep"
|
||||||
|
:class="{ active: isMenuOpen && selectedIndex === 3 }"
|
||||||
|
@click="$emit('sleep')"
|
||||||
|
:disabled="disabled"
|
||||||
|
title="Sleep"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -12,6 +35,14 @@ const props = defineProps({
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
isMenuOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
selectedIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -36,10 +67,20 @@ defineEmits(['stats', 'feed', 'play', 'sleep']);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
opacity: 0.5; /* Default dim */
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn.active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
filter: drop-shadow(0 0 2px rgba(0,0,0,0.5));
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-btn:disabled {
|
.icon-btn:disabled {
|
||||||
opacity: 0.3;
|
opacity: 0.2;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,64 @@
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||||
|
import { SPRITE_PRESETS } from '../data/petPresets.js';
|
||||||
|
|
||||||
|
const CONFIG = SPRITE_PRESETS.tinyTigerCatB;
|
||||||
|
|
||||||
export function usePetSystem() {
|
export function usePetSystem() {
|
||||||
// --- State ---
|
// --- State ---
|
||||||
const stage = ref('egg'); // egg, baby, adult
|
const stage = ref('egg'); // egg, baby, child, adult
|
||||||
const state = ref('idle'); // idle, sleep, eating, sick, dead, refuse
|
const state = ref('idle'); // idle, sleep, eating, sick, dead, refuse
|
||||||
|
|
||||||
// --- Stats ---
|
// --- Stats ---
|
||||||
const stats = ref({
|
const stats = ref({
|
||||||
hunger: 100, // 0-100 (0 = Starving)
|
hunger: CONFIG.needs.hunger.startValue,
|
||||||
happiness: 100, // 0-100 (0 = Depressed)
|
happiness: CONFIG.needs.happiness.startValue,
|
||||||
health: 100, // 0-100 (0 = Sick risk)
|
cleanliness: CONFIG.needs.cleanliness.startValue,
|
||||||
weight: 500, // grams
|
energy: CONFIG.needs.energy.startValue,
|
||||||
age: 0, // days
|
health: 100,
|
||||||
poopCount: 0 // Number of poops on screen
|
poopCount: 0,
|
||||||
|
ageMinutes: 0, // Track age in minutes
|
||||||
|
|
||||||
|
// Tracking for evolution
|
||||||
|
hungerEvents: 0,
|
||||||
|
sicknessEvents: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Internal Timers ---
|
// --- Internal Timers ---
|
||||||
let gameLoopId = null;
|
let gameLoopId = null;
|
||||||
const TICK_RATE = 3000; // 3 seconds per tick
|
const TICK_RATE = 1000; // 1 second per tick for easier calculation
|
||||||
|
const SECONDS_PER_MINUTE = 60; // Game time scale (1 real sec = 1 game sec for now)
|
||||||
|
|
||||||
const isCleaning = ref(false);
|
const isCleaning = ref(false);
|
||||||
|
|
||||||
|
// --- Computed Helpers ---
|
||||||
|
const currentStageConfig = computed(() => {
|
||||||
|
return CONFIG.lifecycle.stages.find(s => s.id === stage.value);
|
||||||
|
});
|
||||||
|
|
||||||
// --- Actions ---
|
// --- Actions ---
|
||||||
|
|
||||||
function feed() {
|
function feed() {
|
||||||
if (state.value === 'sleep' || state.value === 'dead' || stage.value === 'egg' || isCleaning.value) return false;
|
if (state.value === 'sleep' || state.value === 'dead' || stage.value === 'egg' || isCleaning.value) return false;
|
||||||
|
|
||||||
|
// Check refusal (sick or full)
|
||||||
if (state.value === 'sick' || stats.value.hunger >= 90) {
|
if (state.value === 'sick' || stats.value.hunger >= 90) {
|
||||||
// Refuse food if sick or full
|
|
||||||
triggerState('refuse', 2000);
|
triggerState('refuse', 2000);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eat
|
// Eat
|
||||||
triggerState('eating', 3000); // Animation duration
|
triggerState('eating', 3000);
|
||||||
stats.value.hunger = Math.min(100, stats.value.hunger + 20);
|
|
||||||
stats.value.weight += 50;
|
|
||||||
|
|
||||||
// Chance to poop after eating (降低機率)
|
// Recover hunger
|
||||||
if (Math.random() < 0.15) { // 從 0.3 降到 0.15
|
stats.value.hunger = Math.min(CONFIG.needs.hunger.max, stats.value.hunger + CONFIG.needs.hunger.feedRecover);
|
||||||
|
|
||||||
|
// Poop logic
|
||||||
|
// Simplified: Chance to poop based on feed count or random
|
||||||
|
if (Math.random() < (1 / CONFIG.needs.poop.feedsPerPoop)) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (stats.value.poopCount < 4) {
|
if (stats.value.poopCount < CONFIG.needs.poop.maxPoopOnScreen) {
|
||||||
stats.value.poopCount++;
|
stats.value.poopCount++;
|
||||||
|
stats.value.cleanliness -= CONFIG.needs.cleanliness.decayPerPoop;
|
||||||
}
|
}
|
||||||
}, 4000);
|
}, 4000);
|
||||||
}
|
}
|
||||||
|
|
@ -52,9 +69,9 @@ export function usePetSystem() {
|
||||||
function play() {
|
function play() {
|
||||||
if (state.value !== 'idle' || stage.value === 'egg' || isCleaning.value) return false;
|
if (state.value !== 'idle' || stage.value === 'egg' || isCleaning.value) return false;
|
||||||
|
|
||||||
stats.value.happiness = Math.min(100, stats.value.happiness + 15);
|
stats.value.happiness = Math.min(CONFIG.needs.happiness.max, stats.value.happiness + CONFIG.needs.happiness.playRecover);
|
||||||
stats.value.weight -= 10; // Exercise burns calories
|
stats.value.energy = Math.max(0, stats.value.energy - 5); // Playing tires you out
|
||||||
stats.value.hunger = Math.max(0, stats.value.hunger - 5);
|
stats.value.hunger = Math.max(0, stats.value.hunger - 5); // Playing makes you hungry
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,12 +79,12 @@ export function usePetSystem() {
|
||||||
if (stats.value.poopCount > 0 && !isCleaning.value) {
|
if (stats.value.poopCount > 0 && !isCleaning.value) {
|
||||||
isCleaning.value = true;
|
isCleaning.value = true;
|
||||||
|
|
||||||
// Delay removal for animation
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
stats.value.poopCount = 0;
|
stats.value.poopCount = 0;
|
||||||
stats.value.happiness += 10;
|
stats.value.cleanliness = CONFIG.needs.cleanliness.max;
|
||||||
|
stats.value.happiness = Math.min(CONFIG.needs.happiness.max, stats.value.happiness + 10);
|
||||||
isCleaning.value = false;
|
isCleaning.value = false;
|
||||||
}, 2000); // 2 seconds flush animation
|
}, 2000);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -80,74 +97,116 @@ export function usePetSystem() {
|
||||||
if (state.value === 'idle') {
|
if (state.value === 'idle') {
|
||||||
state.value = 'sleep';
|
state.value = 'sleep';
|
||||||
} else if (state.value === 'sleep') {
|
} else if (state.value === 'sleep') {
|
||||||
state.value = 'idle'; // Wake up
|
state.value = 'idle';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Game Loop ---
|
// --- Game Loop ---
|
||||||
function tick() {
|
function tick() {
|
||||||
if (state.value === 'dead' || stage.value === 'egg') return;
|
if (state.value === 'dead') return;
|
||||||
|
|
||||||
// Decrease stats naturally
|
// 1. Age Growth
|
||||||
// 目標:飢餓值約 30-60 分鐘下降 10%,快樂值約 20-40 分鐘下降 10%
|
// Add 1/60th of a minute (since tick is 1 sec)
|
||||||
// TICK_RATE = 3000ms (3秒), 600 ticks = 30分鐘
|
stats.value.ageMinutes += (TICK_RATE / 1000) / 60;
|
||||||
// 飢餓值每 tick -0.05 → 600 ticks = -30 (30分鐘下降30%)
|
|
||||||
// 快樂值每 tick -0.08 → 600 ticks = -48 (30分鐘下降48%)
|
// Check Evolution
|
||||||
|
checkEvolution();
|
||||||
|
|
||||||
|
if (stage.value === 'egg') return; // Egg doesn't have needs decay
|
||||||
|
|
||||||
|
// 2. Needs Decay (Per Minute converted to Per Tick)
|
||||||
|
const decayFactor = (TICK_RATE / 1000) / 60; // Fraction of a minute passed
|
||||||
|
|
||||||
if (state.value !== 'sleep') {
|
if (state.value !== 'sleep') {
|
||||||
stats.value.hunger = Math.max(0, stats.value.hunger - 0.05);
|
stats.value.hunger = Math.max(0, stats.value.hunger - CONFIG.needs.hunger.decayPerMinute * decayFactor);
|
||||||
stats.value.happiness = Math.max(0, stats.value.happiness - 0.08);
|
stats.value.happiness = Math.max(0, stats.value.happiness - CONFIG.needs.happiness.decayPerMinute * decayFactor);
|
||||||
|
stats.value.energy = Math.max(0, stats.value.energy - CONFIG.needs.energy.decayPerMinuteActive * decayFactor);
|
||||||
} else {
|
} else {
|
||||||
// Slower decay when sleeping (約 1/3 速度)
|
// Sleeping recovers energy, slower hunger decay
|
||||||
stats.value.hunger = Math.max(0, stats.value.hunger - 0.015);
|
stats.value.energy = Math.min(CONFIG.needs.energy.max, stats.value.energy + CONFIG.needs.energy.recoverPerMinuteSleep * decayFactor);
|
||||||
stats.value.happiness = Math.max(0, stats.value.happiness - 0.025);
|
stats.value.hunger = Math.max(0, stats.value.hunger - (CONFIG.needs.hunger.decayPerMinute * 0.5) * decayFactor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Random poop generation (更低的機率:約 0.5% per tick)
|
// 3. Health Logic
|
||||||
// 平均約每 200 ticks = 10 分鐘拉一次
|
// Poop penalty
|
||||||
if (state.value !== 'sleep' && Math.random() < 0.005 && stats.value.poopCount < 4 && !isCleaning.value) {
|
|
||||||
stats.value.poopCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Health Logic (更溫和的健康下降)
|
|
||||||
// 便便影響健康:每個便便每 tick -0.1 health
|
|
||||||
if (stats.value.poopCount > 0) {
|
if (stats.value.poopCount > 0) {
|
||||||
stats.value.health = Math.max(0, stats.value.health - (0.1 * stats.value.poopCount));
|
stats.value.health = Math.max(0, stats.value.health - (0.1 * stats.value.poopCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 飢餓影響健康:飢餓值低於 20 時開始影響健康
|
// Starvation penalty
|
||||||
if (stats.value.hunger < 20) {
|
if (stats.value.hunger < CONFIG.needs.hunger.criticalThreshold) {
|
||||||
const hungerPenalty = (20 - stats.value.hunger) * 0.02; // 飢餓越嚴重,扣越多
|
stats.value.health = Math.max(0, stats.value.health - 0.05);
|
||||||
stats.value.health = Math.max(0, stats.value.health - hungerPenalty);
|
stats.value.hungerEvents++; // Track for evolution
|
||||||
}
|
}
|
||||||
|
|
||||||
// 不開心影響健康:快樂值低於 20 時開始影響健康(較輕微)
|
// Sickness Chance
|
||||||
if (stats.value.happiness < 20) {
|
let sickChance = CONFIG.needs.sickness.baseChancePerMinute * decayFactor;
|
||||||
const happinessPenalty = (20 - stats.value.happiness) * 0.01;
|
if (stats.value.cleanliness < CONFIG.needs.cleanliness.criticalThreshold) sickChance += CONFIG.needs.sickness.extraChanceIfDirty * decayFactor;
|
||||||
stats.value.health = Math.max(0, stats.value.health - happinessPenalty);
|
if (stats.value.hunger < CONFIG.needs.hunger.criticalThreshold) sickChance += CONFIG.needs.sickness.extraChanceIfStarving * decayFactor;
|
||||||
}
|
|
||||||
|
|
||||||
// Sickness Check (更低的生病機率)
|
if (Math.random() < sickChance && state.value !== 'sick') {
|
||||||
if (stats.value.health < 30 && state.value !== 'sick') {
|
|
||||||
if (Math.random() < 0.1) { // 從 0.3 降到 0.1
|
|
||||||
state.value = 'sick';
|
state.value = 'sick';
|
||||||
|
stats.value.sicknessEvents++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Mood Update (Simplified)
|
||||||
|
updateMood();
|
||||||
|
|
||||||
|
// 5. Random Events
|
||||||
|
checkRandomEvents(decayFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkEvolution() {
|
||||||
|
// Current stage max age check
|
||||||
|
if (currentStageConfig.value && currentStageConfig.value.maxAgeMinutes !== Infinity) {
|
||||||
|
if (stats.value.ageMinutes >= currentStageConfig.value.maxAgeMinutes) {
|
||||||
|
// Find next stage
|
||||||
|
// Simple linear progression for now, or use evolutionRules
|
||||||
|
const currentIndex = CONFIG.lifecycle.stages.findIndex(s => s.id === stage.value);
|
||||||
|
if (currentIndex < CONFIG.lifecycle.stages.length - 1) {
|
||||||
|
const nextStage = CONFIG.lifecycle.stages[currentIndex + 1];
|
||||||
|
evolveTo(nextStage.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health Recovery (健康值可以緩慢恢復)
|
function evolveTo(newStageId) {
|
||||||
// 如果沒有便便、飢餓值和快樂值都高,健康值會緩慢恢復
|
console.log(`Evolving from ${stage.value} to ${newStageId}`);
|
||||||
if (stats.value.poopCount === 0 && stats.value.hunger > 50 && stats.value.happiness > 50 && stats.value.health < 100 && state.value !== 'sick') {
|
stage.value = newStageId;
|
||||||
stats.value.health = Math.min(100, stats.value.health + 0.05);
|
triggerState('idle', 1000); // Celebration?
|
||||||
}
|
}
|
||||||
|
|
||||||
// Death Check (移除死亡機制,依照之前的討論)
|
function updateMood() {
|
||||||
// if (stats.value.health === 0) {
|
// Determine mood based on CONFIG.mood.states
|
||||||
// state.value = 'dead';
|
// Priority: Angry > Sad > Happy > Neutral
|
||||||
// }
|
const m = CONFIG.mood.states;
|
||||||
|
|
||||||
// Evolution / Growth (Simple Age increment)
|
// This is just internal state tracking, visual update happens in PetGame via props or events
|
||||||
// In a real game, 1 day might be 24h, here maybe every 100 ticks?
|
// For now we just log or emit if needed
|
||||||
// For now, let's just say age increases slowly.
|
}
|
||||||
|
|
||||||
|
function checkRandomEvents(decayFactor) {
|
||||||
|
CONFIG.randomEvents.forEach(event => {
|
||||||
|
const chance = event.chancePerMinute * decayFactor;
|
||||||
|
if (Math.random() < chance) {
|
||||||
|
// Check conditions
|
||||||
|
let conditionMet = true;
|
||||||
|
if (event.condition) {
|
||||||
|
if (event.condition.minEnergy && stats.value.energy < event.condition.minEnergy) conditionMet = false;
|
||||||
|
// Add other condition checks
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditionMet) {
|
||||||
|
console.log('Random Event:', event.message);
|
||||||
|
// Apply effects
|
||||||
|
if (event.effect) {
|
||||||
|
if (event.effect.happiness) stats.value.happiness += event.effect.happiness;
|
||||||
|
if (event.effect.energy) stats.value.energy += event.effect.energy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helpers ---
|
// --- Helpers ---
|
||||||
|
|
@ -155,7 +214,7 @@ export function usePetSystem() {
|
||||||
const previousState = state.value;
|
const previousState = state.value;
|
||||||
state.value = tempState;
|
state.value = tempState;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (state.value === tempState) { // Only revert if state hasn't changed again
|
if (state.value === tempState) {
|
||||||
state.value = previousState === 'sleep' ? 'idle' : 'idle';
|
state.value = previousState === 'sleep' ? 'idle' : 'idle';
|
||||||
}
|
}
|
||||||
}, duration);
|
}, duration);
|
||||||
|
|
@ -163,15 +222,14 @@ export function usePetSystem() {
|
||||||
|
|
||||||
function hatchEgg() {
|
function hatchEgg() {
|
||||||
if (stage.value === 'egg') {
|
if (stage.value === 'egg') {
|
||||||
stage.value = 'baby'; // or 'adult' for now since we only have that sprite
|
// Force evolve to baby
|
||||||
// Let's map 'baby' to our 'adult' sprite for now, or just use 'adult'
|
evolveTo('baby');
|
||||||
stage.value = 'adult';
|
|
||||||
state.value = 'idle';
|
// Reset stats for baby
|
||||||
stats.value.hunger = 50;
|
stats.value.hunger = CONFIG.needs.hunger.startValue;
|
||||||
stats.value.happiness = 50;
|
stats.value.happiness = CONFIG.needs.happiness.startValue;
|
||||||
stats.value.health = 100;
|
stats.value.energy = CONFIG.needs.energy.startValue;
|
||||||
stats.value.poopCount = 0;
|
stats.value.cleanliness = CONFIG.needs.cleanliness.startValue;
|
||||||
isCleaning.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,12 +238,15 @@ export function usePetSystem() {
|
||||||
state.value = 'idle';
|
state.value = 'idle';
|
||||||
isCleaning.value = false;
|
isCleaning.value = false;
|
||||||
stats.value = {
|
stats.value = {
|
||||||
hunger: 100,
|
hunger: CONFIG.needs.hunger.startValue,
|
||||||
happiness: 100,
|
happiness: CONFIG.needs.happiness.startValue,
|
||||||
|
cleanliness: CONFIG.needs.cleanliness.startValue,
|
||||||
|
energy: CONFIG.needs.energy.startValue,
|
||||||
health: 100,
|
health: 100,
|
||||||
weight: 500,
|
poopCount: 0,
|
||||||
age: 0,
|
ageMinutes: 0,
|
||||||
poopCount: 0
|
hungerEvents: 0,
|
||||||
|
sicknessEvents: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,165 +2,123 @@
|
||||||
// 定義各種寵物的像素藝術數據
|
// 定義各種寵物的像素藝術數據
|
||||||
|
|
||||||
export const SPRITE_PRESETS = {
|
export const SPRITE_PRESETS = {
|
||||||
tigerChick: {
|
|
||||||
name: 'tigerChick',
|
|
||||||
pixelSize: 3,
|
|
||||||
sprite: [
|
|
||||||
'1100000111000000',
|
|
||||||
'1241111331000000',
|
|
||||||
'1005102301000000',
|
|
||||||
'1054103320000000',
|
|
||||||
'1241143320100000',
|
|
||||||
'1230432311100110',
|
|
||||||
'1245321330100421',
|
|
||||||
'1240001311100111',
|
|
||||||
'1020103030111421',
|
|
||||||
'0100000331245210',
|
|
||||||
'0001111111240210',
|
|
||||||
'0015022221345210',
|
|
||||||
'0004022235350010',
|
|
||||||
'0011400023203210',
|
|
||||||
'0005011112115210',
|
|
||||||
'0001100001100110',
|
|
||||||
],
|
|
||||||
// 張嘴版:修改嘴巴部分的像素(row 8-9 是嘴巴區域)
|
|
||||||
spriteMouthOpen: [
|
|
||||||
'1100000111000000', // Row 0
|
|
||||||
'1241111331000000', // Row 1
|
|
||||||
'1005102301000000', // Row 2
|
|
||||||
'1054103320000000', // Row 3
|
|
||||||
'1241143320100000', // Row 4
|
|
||||||
'1230432311100110', // Row 5
|
|
||||||
'1245321330100421', // Row 6 - 保持不變
|
|
||||||
'1240001311100111', // Row 7 - 保持不變
|
|
||||||
'1000000030111421', // Row 8 - 嘴巴張開(更往前,移除位置2-7)
|
|
||||||
'0000000331245210', // Row 9 - 嘴巴張開(更往前,移除位置1-7)
|
|
||||||
'0001111111240210', // Row 10
|
|
||||||
'0015022221345210', // Row 11
|
|
||||||
'0004022235350010', // Row 12
|
|
||||||
'0011400023203210', // Row 13
|
|
||||||
'0005011112115210', // Row 14
|
|
||||||
'0001100001100110', // Row 15
|
|
||||||
],
|
|
||||||
palette: {
|
|
||||||
'1': '#2b2825',
|
|
||||||
'2': '#d0974b',
|
|
||||||
'3': '#e09037',
|
|
||||||
'4': '#4a2b0d',
|
|
||||||
'5': '#724e22',
|
|
||||||
},
|
|
||||||
tailPixels: [
|
|
||||||
[15, 8], [14, 8],
|
|
||||||
[15, 9], [14, 9],
|
|
||||||
[15, 10], [14, 10],
|
|
||||||
[15, 11], [14, 11],
|
|
||||||
],
|
|
||||||
legFrontPixels: [
|
|
||||||
[6, 13], [7, 13],
|
|
||||||
[6, 14], [7, 14],
|
|
||||||
],
|
|
||||||
legBackPixels: [
|
|
||||||
[9, 13], [10, 13],
|
|
||||||
[9, 14], [10, 14],
|
|
||||||
],
|
|
||||||
earPixels: [
|
|
||||||
[2, 0], [3, 0], [4, 0],
|
|
||||||
[11, 0], [12, 0], [13, 0],
|
|
||||||
],
|
|
||||||
blushPixels: [
|
|
||||||
[4, 7], [5, 7],
|
|
||||||
[10, 7], [11, 7],
|
|
||||||
],
|
|
||||||
iconBackLeft: { x: 3, y: 2 },
|
|
||||||
iconBackRight: { x: 12, y: 2 },
|
|
||||||
},
|
|
||||||
tinyTigerCat: {
|
|
||||||
name: '小虎斑貓',
|
|
||||||
pixelSize: 3,
|
|
||||||
sprite: [
|
|
||||||
'0000000000000000',
|
|
||||||
'0011000000110000', // row 1 - Ears
|
|
||||||
'0122111111221000', // row 2
|
|
||||||
'0122222222221000', // row 3
|
|
||||||
'0122322223221000', // row 4 - Stripes
|
|
||||||
'0122222222221000', // row 5
|
|
||||||
'0120022220021000', // row 6 - Eyes
|
|
||||||
'0122223322221000', // row 7 - Nose/Mouth
|
|
||||||
'0122222222221000', // row 8
|
|
||||||
'0011222222110000', // row 9 - Body
|
|
||||||
'0001222222121000', // row 10 - Body + Tail
|
|
||||||
'0001222222121000', // row 11
|
|
||||||
'0001100110110000', // row 12 - Legs
|
|
||||||
'0000000000000000', // row 13
|
|
||||||
'0000000000000000', // row 14
|
|
||||||
'0000000000000000', // row 15
|
|
||||||
],
|
|
||||||
spriteMouthOpen: [
|
|
||||||
'0000000000000000',
|
|
||||||
'0011000000110000',
|
|
||||||
'0122111111221000',
|
|
||||||
'0122222222221000',
|
|
||||||
'0122322223221000',
|
|
||||||
'0122222222221000',
|
|
||||||
'0120022220021000',
|
|
||||||
'0122223322221000',
|
|
||||||
'0122200002221000', // Mouth Open
|
|
||||||
'0011222222110000',
|
|
||||||
'0001222222121000',
|
|
||||||
'0001222222121000',
|
|
||||||
'0001100110110000',
|
|
||||||
'0000000000000000',
|
|
||||||
'0000000000000000',
|
|
||||||
'0000000000000000',
|
|
||||||
],
|
|
||||||
palette: {
|
|
||||||
'0': '#000000', // Black eyes/outline
|
|
||||||
'1': '#2b1d12', // Dark brown outline
|
|
||||||
'2': '#ffb347', // Orange fur
|
|
||||||
'3': '#cd853f', // Darker stripes/nose
|
|
||||||
},
|
|
||||||
tailPixels: [
|
|
||||||
[11, 10], [12, 10],
|
|
||||||
[11, 11], [12, 11],
|
|
||||||
],
|
|
||||||
earPixels: [
|
|
||||||
[2, 1], [3, 1],
|
|
||||||
[10, 1], [11, 1],
|
|
||||||
],
|
|
||||||
legFrontPixels: [
|
|
||||||
[4, 12], [5, 12],
|
|
||||||
],
|
|
||||||
legBackPixels: [
|
|
||||||
[8, 12], [9, 12],
|
|
||||||
],
|
|
||||||
blushPixels: [
|
|
||||||
[3, 7], [10, 7]
|
|
||||||
],
|
|
||||||
iconBackLeft: { x: 2, y: 2 },
|
|
||||||
iconBackRight: { x: 11, y: 2 }
|
|
||||||
},
|
|
||||||
tinyTigerCatB: {
|
tinyTigerCatB: {
|
||||||
|
id: 'tinyTigerCatB',
|
||||||
|
meta: {
|
||||||
name: '小虎斑貓',
|
name: '小虎斑貓',
|
||||||
pixelSize: 3,
|
displayNameEn: 'Tiny Tiger Cat',
|
||||||
sprite: [
|
species: 'cat',
|
||||||
'0000000000000000',
|
element: 'normal',
|
||||||
'0011000000110000', // row 1 - Ears
|
description: '一隻活潑、黏人的小虎斑貓,喜歡被餵食和玩耍。'
|
||||||
'0124444111442100', // row 2 粉紅耳朵內側
|
},
|
||||||
'0123222323221000', // row 3 三條虎紋
|
lifecycle: {
|
||||||
'0122322223221000', // row 4 - Stripes
|
baseLifeMinutes: 7 * 24 * 60,
|
||||||
'0122522222522100', // row 5 眼睛反光
|
stages: [
|
||||||
'0125052225052100', // row 6 大圓眼+黑瞳孔+白反光
|
{
|
||||||
'0112223322221100', // row 7 鼻子+左右鬍鬚
|
id: 'egg',
|
||||||
'0122220222221000', // row 8 小微笑
|
name: '蛋',
|
||||||
'0011222222110000', // row 9 - Body
|
minAgeMinutes: 0,
|
||||||
'0001222222121000', // row 10 - Body + Tail
|
maxAgeMinutes: 30,
|
||||||
'0001222222121000', // row 11
|
spriteKey: 'egg',
|
||||||
'0001100110110000', // row 12 - Legs
|
canBattle: false,
|
||||||
'0000000000000000', // row 13
|
canEquip: false
|
||||||
'0000000000000000', // row 14
|
},
|
||||||
'0000000000000000', // row 15
|
{
|
||||||
|
id: 'baby',
|
||||||
|
name: '幼年期',
|
||||||
|
minAgeMinutes: 30,
|
||||||
|
maxAgeMinutes: 6 * 60,
|
||||||
|
spriteKey: 'child',
|
||||||
|
canBattle: false,
|
||||||
|
canEquip: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'child',
|
||||||
|
name: '成長期',
|
||||||
|
minAgeMinutes: 6 * 60,
|
||||||
|
maxAgeMinutes: 24 * 60,
|
||||||
|
spriteKey: 'child',
|
||||||
|
canBattle: true,
|
||||||
|
canEquip: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'adult',
|
||||||
|
name: '成熟期',
|
||||||
|
minAgeMinutes: 24 * 60,
|
||||||
|
maxAgeMinutes: Infinity,
|
||||||
|
spriteKey: 'adult',
|
||||||
|
canBattle: true,
|
||||||
|
canEquip: true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
spriteMouthOpen: [
|
evolutionRules: [
|
||||||
|
{
|
||||||
|
fromStage: 'baby',
|
||||||
|
toStage: 'child',
|
||||||
|
condition: {
|
||||||
|
maxHungerEvents: 5,
|
||||||
|
maxSicknessEvents: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
needs: {
|
||||||
|
hunger: {
|
||||||
|
max: 100,
|
||||||
|
startValue: 70,
|
||||||
|
decayPerMinute: 2,
|
||||||
|
warnThreshold: 40,
|
||||||
|
criticalThreshold: 10,
|
||||||
|
feedRecover: 40
|
||||||
|
},
|
||||||
|
happiness: {
|
||||||
|
max: 100,
|
||||||
|
startValue: 60,
|
||||||
|
decayPerMinute: 1,
|
||||||
|
playRecover: 25,
|
||||||
|
lowThreshold: 30
|
||||||
|
},
|
||||||
|
cleanliness: {
|
||||||
|
max: 100,
|
||||||
|
startValue: 80,
|
||||||
|
decayPerPoop: 30,
|
||||||
|
criticalThreshold: 30
|
||||||
|
},
|
||||||
|
energy: {
|
||||||
|
max: 100,
|
||||||
|
startValue: 80,
|
||||||
|
decayPerMinuteActive: 2,
|
||||||
|
recoverPerMinuteSleep: 5,
|
||||||
|
sleepSuggestThreshold: 30
|
||||||
|
},
|
||||||
|
poop: {
|
||||||
|
feedsPerPoop: 3,
|
||||||
|
maxPoopOnScreen: 3
|
||||||
|
},
|
||||||
|
sickness: {
|
||||||
|
baseChancePerMinute: 0.001,
|
||||||
|
extraChanceIfDirty: 0.01,
|
||||||
|
extraChanceIfStarving: 0.02
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
base: {
|
||||||
|
hp: 30,
|
||||||
|
attack: 8,
|
||||||
|
defense: 5,
|
||||||
|
speed: 7
|
||||||
|
},
|
||||||
|
stageModifiers: {
|
||||||
|
baby: { hp: 0.6, attack: 0.5, defense: 0.5, speed: 0.8 },
|
||||||
|
child: { hp: 1.0, attack: 1.0, defense: 1.0, speed: 1.0 },
|
||||||
|
adult: { hp: 1.4, attack: 1.3, defense: 1.2, speed: 1.1 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
appearance: {
|
||||||
|
pixelSize: 3,
|
||||||
|
sprites: {
|
||||||
|
child: {
|
||||||
|
idle: [
|
||||||
'0000000000000000',
|
'0000000000000000',
|
||||||
'0011000000110000',
|
'0011000000110000',
|
||||||
'0124444111442100',
|
'0124444111442100',
|
||||||
|
|
@ -169,7 +127,7 @@ export const SPRITE_PRESETS = {
|
||||||
'0122522222522100',
|
'0122522222522100',
|
||||||
'0125052225052100',
|
'0125052225052100',
|
||||||
'0112223322221100',
|
'0112223322221100',
|
||||||
'0122204002221000', // Mouth Open 粉紅舌頭
|
'0122220222221000',
|
||||||
'0011222222110000',
|
'0011222222110000',
|
||||||
'0001222222121000',
|
'0001222222121000',
|
||||||
'0001222222121000',
|
'0001222222121000',
|
||||||
|
|
@ -178,14 +136,100 @@ export const SPRITE_PRESETS = {
|
||||||
'0000000000000000',
|
'0000000000000000',
|
||||||
'0000000000000000',
|
'0000000000000000',
|
||||||
],
|
],
|
||||||
palette: {
|
mouthOpen: [
|
||||||
'0': '#000000', // Black eyes/outline
|
'0000000000000000',
|
||||||
'1': '#2b1d12', // Dark brown outline
|
'0011000000110000',
|
||||||
'2': '#ffb347', // Orange fur
|
'0124444111442100',
|
||||||
'3': '#cd853f', // Darker stripes/nose
|
'0123222323221000',
|
||||||
'4': '#ffb6c1', // Pink (ears, blush, tongue)
|
'0122322223221000',
|
||||||
'5': '#ffffff' // White eye highlight
|
'0122522222522100',
|
||||||
|
'0125052225052100',
|
||||||
|
'0112223322221100',
|
||||||
|
'0122204002221000',
|
||||||
|
'0011222222110000',
|
||||||
|
'0001222222121000',
|
||||||
|
'0001222222121000',
|
||||||
|
'0001100110110000',
|
||||||
|
'0000000000000000',
|
||||||
|
'0000000000000000',
|
||||||
|
'0000000000000000',
|
||||||
|
],
|
||||||
|
eyesClosed: [
|
||||||
|
'0000000000000000',
|
||||||
|
'0011000000110000',
|
||||||
|
'0124444111442100',
|
||||||
|
'0123222323221000',
|
||||||
|
'0122322223221000',
|
||||||
|
'0122522222522100',
|
||||||
|
'0122222222222100',
|
||||||
|
'0112223322221100',
|
||||||
|
'0122220222221000',
|
||||||
|
'0011222222110000',
|
||||||
|
'0001222222121000',
|
||||||
|
'0001222222121000',
|
||||||
|
'0001100110110000',
|
||||||
|
'0000000000000000',
|
||||||
|
'0000000000000000',
|
||||||
|
'0000000000000000',
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
egg: {
|
||||||
|
idle: [
|
||||||
|
'0000000000000000',
|
||||||
|
'0000000000000000',
|
||||||
|
'0000000111000000',
|
||||||
|
'0000001222100000',
|
||||||
|
'0000012232210000',
|
||||||
|
'0000122333221000',
|
||||||
|
'0000122232221000',
|
||||||
|
'0001222222222100',
|
||||||
|
'0001233322332100',
|
||||||
|
'0001223222232100',
|
||||||
|
'0000122222221000',
|
||||||
|
'0000122222221000',
|
||||||
|
'0000011222110000',
|
||||||
|
'0000000111000000',
|
||||||
|
'0000000000000000',
|
||||||
|
'0000000000000000',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
adult: {
|
||||||
|
idle: [
|
||||||
|
'0000000000000000',
|
||||||
|
'0011000000110000',
|
||||||
|
'0124444111442100',
|
||||||
|
'0123222323221000',
|
||||||
|
'0122322223221000',
|
||||||
|
'0122522222522100',
|
||||||
|
'0125052225052100',
|
||||||
|
'0112223322221100',
|
||||||
|
'0122220222221000',
|
||||||
|
'0011222222110000',
|
||||||
|
'0001222222121000',
|
||||||
|
'0001222222121000',
|
||||||
|
'0001100110110000',
|
||||||
|
'0000000000000000',
|
||||||
|
'0000000000000000',
|
||||||
|
'0000000000000000',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
palettes: {
|
||||||
|
default: {
|
||||||
|
'0': '#000000',
|
||||||
|
'1': '#2b1d12',
|
||||||
|
'2': '#ffb347',
|
||||||
|
'3': '#cd853f',
|
||||||
|
'4': '#ffb6c1',
|
||||||
|
'5': '#ffffff'
|
||||||
|
},
|
||||||
|
egg: {
|
||||||
|
'1': '#5d4037',
|
||||||
|
'2': '#fff8e1',
|
||||||
|
'3': '#ffb74d',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bodyParts: {
|
||||||
tailPixels: [
|
tailPixels: [
|
||||||
[11, 10], [12, 10],
|
[11, 10], [12, 10],
|
||||||
[11, 11], [12, 11],
|
[11, 11], [12, 11],
|
||||||
|
|
@ -204,53 +248,77 @@ export const SPRITE_PRESETS = {
|
||||||
[3, 7], [10, 7]
|
[3, 7], [10, 7]
|
||||||
],
|
],
|
||||||
eyePixels: [
|
eyePixels: [
|
||||||
[3, 6], [4, 6], // Left eye
|
[3, 6], [4, 6],
|
||||||
[8, 6], [9, 6] // Right eye
|
[8, 6], [9, 6]
|
||||||
],
|
|
||||||
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 }
|
||||||
|
},
|
||||||
// Growth Stages
|
behaviorAnimation: {
|
||||||
eggSprite: [
|
blinkIntervalSec: 5,
|
||||||
'0000000000000000',
|
blinkDurationMs: 200,
|
||||||
'0000000000000000',
|
mouthOpenDurationMs: 300,
|
||||||
'0000000111000000', // Top (Narrow)
|
idleEmoteIntervalSec: 15
|
||||||
'0000001222100000',
|
|
||||||
'0000012232210000', // Small stripe
|
|
||||||
'0000122333221000',
|
|
||||||
'0000122232221000',
|
|
||||||
'0001222222222100', // Widest part
|
|
||||||
'0001233322332100', // Side stripes
|
|
||||||
'0001223222232100',
|
|
||||||
'0000122222221000',
|
|
||||||
'0000122222221000',
|
|
||||||
'0000011222110000', // Bottom
|
|
||||||
'0000000111000000',
|
|
||||||
'0000000000000000',
|
|
||||||
'0000000000000000',
|
|
||||||
],
|
|
||||||
eggPalette: {
|
|
||||||
'1': '#5d4037', // Dark brown outline
|
|
||||||
'2': '#fff8e1', // Creamy white shell
|
|
||||||
'3': '#ffb74d', // Orange tiger stripes
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
equipment: {
|
||||||
|
slots: ['head', 'face', 'neck', 'back'],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'sunglasses_basic',
|
||||||
|
name: '基本墨鏡',
|
||||||
|
slot: 'face',
|
||||||
|
overlays: {
|
||||||
|
child: {
|
||||||
|
pixels: [
|
||||||
|
{ x: 3, y: 6, color: '0' },
|
||||||
|
{ x: 4, y: 6, color: '0' },
|
||||||
|
{ x: 8, y: 6, color: '0' },
|
||||||
|
{ x: 9, y: 6, color: '0' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
statModifiers: {
|
||||||
|
coolness: +10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
personality: {
|
||||||
|
traits: ['clingy', 'energetic'],
|
||||||
|
modifiers: {
|
||||||
|
hungerDecay: 1.0,
|
||||||
|
happinessDecay: 1.2,
|
||||||
|
energyDecay: 0.9
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mood: {
|
||||||
|
system: {
|
||||||
|
updateIntervalMinutes: 10,
|
||||||
|
factors: ['happiness', 'hunger', 'health']
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
happy: { minHappiness: 80, spriteFace: 'happy' },
|
||||||
|
neutral: { minHappiness: 40, spriteFace: 'neutral' },
|
||||||
|
sad: { maxHappiness: 40, spriteFace: 'sad' },
|
||||||
|
angry: { maxHunger: 20, spriteFace: 'angry' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
randomEvents: [
|
||||||
|
{
|
||||||
|
id: 'trip_over',
|
||||||
|
chancePerMinute: 0.005,
|
||||||
|
condition: { maxCoordination: 20 },
|
||||||
|
effect: { happiness: -5, health: -1 },
|
||||||
|
message: 'Tripped and fell!'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'zoomies',
|
||||||
|
chancePerMinute: 0.01,
|
||||||
|
condition: { minEnergy: 90 },
|
||||||
|
effect: { energy: -20, happiness: +10 },
|
||||||
|
message: 'Suddenly started running around!'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue