Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
bae581b778 |
88
src/App.vue
88
src/App.vue
|
|
@ -8,7 +8,16 @@ import { usePetSystem } from './composables/usePetSystem';
|
|||
|
||||
const currentScreen = ref('game');
|
||||
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
|
||||
const {
|
||||
|
|
@ -24,8 +33,48 @@ const {
|
|||
reset
|
||||
} = 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
|
||||
function handleAction(action) {
|
||||
console.log('Action triggered:', action);
|
||||
switch(action) {
|
||||
case 'feed':
|
||||
const feedResult = feed();
|
||||
|
|
@ -51,8 +100,25 @@ function handleAction(action) {
|
|||
}
|
||||
break;
|
||||
case 'stats':
|
||||
// Toggle stats display
|
||||
showStats.value = !showStats.value;
|
||||
// Toggle stats mode: none -> bars -> details -> none
|
||||
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;
|
||||
case 'settings':
|
||||
// Show reset options
|
||||
|
|
@ -62,16 +128,6 @@ function handleAction(action) {
|
|||
reset();
|
||||
}
|
||||
break;
|
||||
case 'jiaobei':
|
||||
// 擲筊功能 - 待實作
|
||||
console.log('擲筊功能');
|
||||
// TODO: 實作擲筊邏輯
|
||||
break;
|
||||
case 'fortune':
|
||||
// 求籤功能 - 待實作
|
||||
console.log('求籤功能');
|
||||
// TODO: 實作求籤邏輯
|
||||
break;
|
||||
default:
|
||||
console.log('Action not implemented:', action);
|
||||
}
|
||||
|
|
@ -84,7 +140,7 @@ function setPetState(newState) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<DeviceShell>
|
||||
<DeviceShell @btn1="handleButton(1)" @btn2="handleButton(2)" @btn3="handleButton(3)">
|
||||
<DeviceScreen>
|
||||
<!-- Dynamic Component Switching -->
|
||||
<PetGame
|
||||
|
|
@ -94,7 +150,9 @@ function setPetState(newState) {
|
|||
:stage="stage"
|
||||
:stats="stats"
|
||||
:isCleaning="isCleaning"
|
||||
:showStats="showStats"
|
||||
:statsMode="statsMode"
|
||||
:isMenuOpen="isMenuOpen"
|
||||
:selectedMenuIndex="selectedMenuIndex"
|
||||
@update:state="state = $event"
|
||||
@action="handleAction"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,33 @@
|
|||
<template>
|
||||
<div class="action-menu">
|
||||
<button class="icon-btn icon-clean" @click="$emit('clean')" :disabled="disabled || poopCount === 0" title="清理"></button>
|
||||
<button class="icon-btn icon-medicine" @click="$emit('medicine')" :disabled="disabled || !isSick" title="治療"></button>
|
||||
<button class="icon-btn icon-training" @click="$emit('training')" :disabled="disabled" title="祈禱"></button>
|
||||
<button class="icon-btn icon-info" @click="$emit('info')" :disabled="disabled" title="資訊"></button>
|
||||
<button
|
||||
class="icon-btn icon-clean"
|
||||
:class="{ active: isMenuOpen && selectedIndex === 4 }"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
|
|
@ -24,6 +48,14 @@ const props = defineProps({
|
|||
isSick: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isMenuOpen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -48,10 +80,20 @@ defineEmits(['clean', 'medicine', 'training', 'info']);
|
|||
cursor: pointer;
|
||||
position: relative;
|
||||
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 {
|
||||
opacity: 0.3;
|
||||
opacity: 0.2;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<div class="btn"></div>
|
||||
<div class="btn"></div>
|
||||
<div class="btn"></div>
|
||||
<div class="btn" @click="$emit('btn1')"></div>
|
||||
<div class="btn" @click="$emit('btn2')"></div>
|
||||
<div class="btn" @click="$emit('btn3')"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -22,13 +22,13 @@
|
|||
</div>
|
||||
|
||||
<div v-if="!isShaking && selectedStick" class="action-buttons">
|
||||
<button class="pixel-btn confirm-btn" @click="handleConfirm">擲筊確認</button>
|
||||
<button class="pixel-btn close-btn" @click="$emit('close')">返回</button>
|
||||
<button class="pixel-btn confirm-btn" :class="{ active: selectedIndex === 0 }" @click="handleConfirm">擲筊確認</button>
|
||||
<button class="pixel-btn close-btn" :class="{ active: selectedIndex === 1 }" @click="$emit('close')">返回</button>
|
||||
</div>
|
||||
|
||||
<!-- 在搖動時也顯示返回按鈕,但位置可能需要調整 -->
|
||||
<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>
|
||||
|
|
@ -37,6 +37,13 @@
|
|||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
selectedIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['complete', 'close']);
|
||||
|
||||
const isShaking = ref(true);
|
||||
|
|
@ -68,6 +75,11 @@ function handleConfirm() {
|
|||
onMounted(() => {
|
||||
startShake();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
hasFallen,
|
||||
handleConfirm
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -220,7 +232,7 @@ onMounted(() => {
|
|||
.status-text {
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
text-shadow: 1px 1px 0 #000;
|
||||
/* text-shadow: 1px 1px 0 #000; Removed to fix ghosting */
|
||||
min-height: 18px;
|
||||
}
|
||||
|
||||
|
|
@ -240,7 +252,7 @@ onMounted(() => {
|
|||
color: #fff;
|
||||
cursor: pointer;
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -254,6 +266,14 @@ onMounted(() => {
|
|||
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 {
|
||||
border-color: #ffcc00;
|
||||
color: #ffcc00;
|
||||
|
|
|
|||
|
|
@ -30,19 +30,19 @@
|
|||
<div v-if="!isTossing" class="action-buttons">
|
||||
<!-- 一般模式 -->
|
||||
<template v-if="mode === 'normal'">
|
||||
<button class="pixel-btn retry-btn" @click="startToss">再一次</button>
|
||||
<button class="pixel-btn close-btn" @click="handleClose">關閉</button>
|
||||
<button class="pixel-btn retry-btn" :class="{ active: selectedIndex === 0 }" @click="startToss">再一次</button>
|
||||
<button class="pixel-btn close-btn" :class="{ active: selectedIndex === 1 }" @click="handleClose">關閉</button>
|
||||
</template>
|
||||
|
||||
<!-- 求籤模式 -->
|
||||
<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>
|
||||
<button v-if="consecutiveCount < 2" class="pixel-btn retry-btn" @click="startToss">繼續擲筊</button>
|
||||
<button v-else class="pixel-btn retry-btn" @click="$emit('finish-fortune')">查看籤詩</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" :class="{ active: selectedIndex === 0 }" @click="$emit('finish-fortune')">查看籤詩</button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -61,6 +61,10 @@ const props = defineProps({
|
|||
consecutiveCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
selectedIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -125,6 +129,10 @@ function handleRetryFortune() {
|
|||
onMounted(() => {
|
||||
startToss();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
startToss
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -264,7 +272,7 @@ onMounted(() => {
|
|||
color: #fff;
|
||||
text-align: center;
|
||||
min-height: 20px;
|
||||
text-shadow: 1px 1px 0 #000;
|
||||
/* text-shadow: 1px 1px 0 #000; Removed to fix ghosting */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
@ -302,7 +310,7 @@ onMounted(() => {
|
|||
color: #fff;
|
||||
cursor: pointer;
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -316,6 +324,14 @@ onMounted(() => {
|
|||
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 {
|
||||
border-color: #ffcc00;
|
||||
color: #ffcc00;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
<!-- Top Menu -->
|
||||
<TopMenu
|
||||
:disabled="stage === 'egg'"
|
||||
:isMenuOpen="isMenuOpen"
|
||||
:selectedIndex="selectedMenuIndex"
|
||||
@stats="$emit('action', 'stats')"
|
||||
@feed="$emit('action', 'feed')"
|
||||
@play="$emit('action', 'play')"
|
||||
|
|
@ -11,12 +13,18 @@
|
|||
|
||||
<!-- Stats Dashboard (Toggelable) -->
|
||||
<StatsBar
|
||||
v-if="showStats"
|
||||
v-if="statsMode === 'bars'"
|
||||
:hunger="stats?.hunger || 100"
|
||||
:happiness="stats?.happiness || 100"
|
||||
:health="stats?.health || 100"
|
||||
/>
|
||||
|
||||
<!-- Detailed Info Overlay -->
|
||||
<PetInfo
|
||||
v-if="statsMode === 'details'"
|
||||
:stats="stats"
|
||||
/>
|
||||
|
||||
<!-- Game Area (Center) -->
|
||||
<div class="pet-game-container" ref="containerRef">
|
||||
<!-- 關燈黑色遮罩 -->
|
||||
|
|
@ -133,6 +141,7 @@
|
|||
<!-- Prayer Menu (覆蓋整個遊戲區域) -->
|
||||
<PrayerMenu
|
||||
v-if="showPrayerMenu"
|
||||
:selectedIndex="prayerMenuIndex"
|
||||
@select="handlePrayerSelect"
|
||||
@close="showPrayerMenu = false"
|
||||
/>
|
||||
|
|
@ -140,8 +149,10 @@
|
|||
<!-- Jiaobei Animation (覆蓋遊戲區域) -->
|
||||
<JiaobeiAnimation
|
||||
v-if="showJiaobeiAnimation"
|
||||
ref="jiaobeiRef"
|
||||
:mode="fortuneMode"
|
||||
:consecutiveCount="consecutiveSaintCount"
|
||||
:selectedIndex="jiaobeiIndex"
|
||||
@close="handleJiaobeiClose"
|
||||
@result="handleJiaobeiResult"
|
||||
@retry-fortune="handleRetryFortune"
|
||||
|
|
@ -151,6 +162,8 @@
|
|||
<!-- Fortune Stick Animation -->
|
||||
<FortuneStickAnimation
|
||||
v-if="showFortuneStick"
|
||||
ref="fortuneStickRef"
|
||||
:selectedIndex="fortuneStickIndex"
|
||||
@complete="handleStickComplete"
|
||||
@close="handleFortuneStickClose"
|
||||
/>
|
||||
|
|
@ -168,6 +181,8 @@
|
|||
:poopCount="stats?.poopCount || 0"
|
||||
:health="stats?.health || 100"
|
||||
:isSick="state === 'sick'"
|
||||
:isMenuOpen="isMenuOpen"
|
||||
:selectedIndex="selectedMenuIndex"
|
||||
@clean="$emit('action', 'clean')"
|
||||
@medicine="$emit('action', 'medicine')"
|
||||
@training="showPrayerMenu = true"
|
||||
|
|
@ -182,6 +197,7 @@ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
|||
import { SPRITE_PRESETS } from '../data/petPresets.js';
|
||||
import { FOOD_OPTIONS } from '../data/foodOptions.js';
|
||||
import StatsBar from './StatsBar.vue';
|
||||
import PetInfo from './PetInfo.vue';
|
||||
import ActionMenu from './ActionMenu.vue';
|
||||
import TopMenu from './TopMenu.vue';
|
||||
import PrayerMenu from './PrayerMenu.vue';
|
||||
|
|
@ -203,13 +219,21 @@ const props = defineProps({
|
|||
type: Object,
|
||||
default: () => ({ hunger: 100, happiness: 100, health: 100, poopCount: 0 })
|
||||
},
|
||||
showStats: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
statsMode: {
|
||||
type: String,
|
||||
default: 'none' // 'none', 'bars', 'details'
|
||||
},
|
||||
isCleaning: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isMenuOpen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedMenuIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -264,16 +288,7 @@ function handleFortuneStickClose() {
|
|||
consecutiveSaintCount.value = 0;
|
||||
}
|
||||
|
||||
function handleJiaobeiResult(type) {
|
||||
if (fortuneMode.value === 'fortune') {
|
||||
if (type === 'saint') {
|
||||
consecutiveSaintCount.value++;
|
||||
} else {
|
||||
// 如果不是聖筊,計數歸零(邏輯上失敗了,需要重新求籤)
|
||||
consecutiveSaintCount.value = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleRetryFortune() {
|
||||
// 重新開始搖籤
|
||||
|
|
@ -379,11 +394,38 @@ const FOOD_PALETTE = FOOD_OPTIONS[currentFood].palette;
|
|||
|
||||
|
||||
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
|
||||
const rows = CURRENT_PRESET.sprite.length;
|
||||
const cols = CURRENT_PRESET.sprite[0].length;
|
||||
// Helper to get current stage config
|
||||
const currentStageConfig = computed(() => {
|
||||
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 height = rows * pixelSize;
|
||||
|
||||
|
|
@ -391,7 +433,9 @@ const height = rows * pixelSize;
|
|||
// 2. Generate Pixels Helper
|
||||
function generatePixels(spriteMap, paletteOverride = null) {
|
||||
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) => {
|
||||
[...rowStr].forEach((ch, x) => {
|
||||
|
|
@ -400,11 +444,14 @@ function generatePixels(spriteMap, paletteOverride = null) {
|
|||
// Only apply body part classes if NOT an egg
|
||||
let className = '';
|
||||
if (props.stage !== 'egg') {
|
||||
const isTail = CURRENT_PRESET.tailPixels?.some(([tx, ty]) => tx === x && ty === y);
|
||||
const isLegFront = CURRENT_PRESET.legFrontPixels?.some(([lx, ly]) => lx === x && ly === y);
|
||||
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 isBlush = CURRENT_PRESET.blushPixels?.some(([bx, by]) => bx === x && by === y);
|
||||
// Handle new bodyParts structure vs old root properties
|
||||
const bodyParts = CURRENT_PRESET.appearance?.bodyParts || CURRENT_PRESET;
|
||||
|
||||
const isTail = bodyParts.tailPixels?.some(([tx, ty]) => tx === x && ty === 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 (isLegFront) className += ' leg-front';
|
||||
|
|
@ -447,12 +494,33 @@ const foodVisible = ref(false);
|
|||
const isBlinking = ref(false);
|
||||
|
||||
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') {
|
||||
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);
|
||||
}
|
||||
|
|
@ -484,7 +552,11 @@ const iconStyle = computed(() => ({
|
|||
}));
|
||||
|
||||
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 baseX = petX.value + marker.x * pixelSize;
|
||||
const baseY = petY.value + marker.y * pixelSize;
|
||||
|
|
@ -786,9 +858,129 @@ async function shakeHead() {
|
|||
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({
|
||||
shakeHead
|
||||
shakeHead,
|
||||
openPrayerMenu,
|
||||
handleInput
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -798,6 +990,7 @@ defineExpose({
|
|||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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
|
||||
class="prayer-option"
|
||||
:class="{ active: selectedIndex === 0 }"
|
||||
@click="$emit('select', 'jiaobei')"
|
||||
>
|
||||
<div class="option-icon icon-jiaobei"></div>
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
<!-- 求籤選項 -->
|
||||
<button
|
||||
class="prayer-option"
|
||||
:class="{ active: selectedIndex === 1 }"
|
||||
@click="$emit('select', 'fortune')"
|
||||
>
|
||||
<div class="option-icon icon-fortune"></div>
|
||||
|
|
@ -23,13 +25,24 @@
|
|||
</div>
|
||||
|
||||
<!-- 返回按鈕 -->
|
||||
<button class="back-button" @click="$emit('close')">
|
||||
<button
|
||||
class="back-button"
|
||||
:class="{ active: selectedIndex === 2 }"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
返回
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
selectedIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
});
|
||||
|
||||
defineEmits(['select', 'close']);
|
||||
</script>
|
||||
|
||||
|
|
@ -79,77 +92,15 @@ defineEmits(['select', 'close']);
|
|||
min-width: 60px; /* 縮小最小寬度 */
|
||||
}
|
||||
|
||||
.prayer-option:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
.prayer-option.active {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
border-color: #fff;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.prayer-option:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.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;
|
||||
.prayer-option.active .option-label {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
|
|
@ -164,11 +115,86 @@ defineEmits(['select', 'close']);
|
|||
font-family: monospace;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
.back-button.active {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.back-button:active {
|
||||
transform: translateY(1px);
|
||||
/* --- Icons (Pixel Art) --- */
|
||||
.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>
|
||||
<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>
|
||||
<button
|
||||
class="icon-btn icon-stats"
|
||||
:class="{ active: isMenuOpen && selectedIndex === 0 }"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
|
|
@ -12,6 +35,14 @@ const props = defineProps({
|
|||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isMenuOpen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -36,10 +67,20 @@ defineEmits(['stats', 'feed', 'play', 'sleep']);
|
|||
cursor: pointer;
|
||||
position: relative;
|
||||
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 {
|
||||
opacity: 0.3;
|
||||
opacity: 0.2;
|
||||
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() {
|
||||
// --- 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
|
||||
|
||||
// --- 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
|
||||
hunger: CONFIG.needs.hunger.startValue,
|
||||
happiness: CONFIG.needs.happiness.startValue,
|
||||
cleanliness: CONFIG.needs.cleanliness.startValue,
|
||||
energy: CONFIG.needs.energy.startValue,
|
||||
health: 100,
|
||||
poopCount: 0,
|
||||
ageMinutes: 0, // Track age in minutes
|
||||
|
||||
// Tracking for evolution
|
||||
hungerEvents: 0,
|
||||
sicknessEvents: 0
|
||||
});
|
||||
|
||||
// --- Internal Timers ---
|
||||
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);
|
||||
|
||||
// --- Computed Helpers ---
|
||||
const currentStageConfig = computed(() => {
|
||||
return CONFIG.lifecycle.stages.find(s => s.id === stage.value);
|
||||
});
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
function feed() {
|
||||
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) {
|
||||
// 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;
|
||||
triggerState('eating', 3000);
|
||||
|
||||
// Chance to poop after eating (降低機率)
|
||||
if (Math.random() < 0.15) { // 從 0.3 降到 0.15
|
||||
// Recover hunger
|
||||
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(() => {
|
||||
if (stats.value.poopCount < 4) {
|
||||
if (stats.value.poopCount < CONFIG.needs.poop.maxPoopOnScreen) {
|
||||
stats.value.poopCount++;
|
||||
stats.value.cleanliness -= CONFIG.needs.cleanliness.decayPerPoop;
|
||||
}
|
||||
}, 4000);
|
||||
}
|
||||
|
|
@ -52,9 +69,9 @@ export function usePetSystem() {
|
|||
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);
|
||||
stats.value.happiness = Math.min(CONFIG.needs.happiness.max, stats.value.happiness + CONFIG.needs.happiness.playRecover);
|
||||
stats.value.energy = Math.max(0, stats.value.energy - 5); // Playing tires you out
|
||||
stats.value.hunger = Math.max(0, stats.value.hunger - 5); // Playing makes you hungry
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -62,12 +79,12 @@ export function usePetSystem() {
|
|||
if (stats.value.poopCount > 0 && !isCleaning.value) {
|
||||
isCleaning.value = true;
|
||||
|
||||
// Delay removal for animation
|
||||
setTimeout(() => {
|
||||
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;
|
||||
}, 2000); // 2 seconds flush animation
|
||||
}, 2000);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -80,74 +97,116 @@ export function usePetSystem() {
|
|||
if (state.value === 'idle') {
|
||||
state.value = 'sleep';
|
||||
} else if (state.value === 'sleep') {
|
||||
state.value = 'idle'; // Wake up
|
||||
state.value = 'idle';
|
||||
}
|
||||
}
|
||||
|
||||
// --- Game Loop ---
|
||||
function tick() {
|
||||
if (state.value === 'dead' || stage.value === 'egg') return;
|
||||
if (state.value === 'dead') return;
|
||||
|
||||
// Decrease stats naturally
|
||||
// 目標:飢餓值約 30-60 分鐘下降 10%,快樂值約 20-40 分鐘下降 10%
|
||||
// TICK_RATE = 3000ms (3秒), 600 ticks = 30分鐘
|
||||
// 飢餓值每 tick -0.05 → 600 ticks = -30 (30分鐘下降30%)
|
||||
// 快樂值每 tick -0.08 → 600 ticks = -48 (30分鐘下降48%)
|
||||
// 1. Age Growth
|
||||
// Add 1/60th of a minute (since tick is 1 sec)
|
||||
stats.value.ageMinutes += (TICK_RATE / 1000) / 60;
|
||||
|
||||
// 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') {
|
||||
stats.value.hunger = Math.max(0, stats.value.hunger - 0.05);
|
||||
stats.value.happiness = Math.max(0, stats.value.happiness - 0.08);
|
||||
stats.value.hunger = Math.max(0, stats.value.hunger - CONFIG.needs.hunger.decayPerMinute * decayFactor);
|
||||
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 {
|
||||
// Slower decay when sleeping (約 1/3 速度)
|
||||
stats.value.hunger = Math.max(0, stats.value.hunger - 0.015);
|
||||
stats.value.happiness = Math.max(0, stats.value.happiness - 0.025);
|
||||
// Sleeping recovers energy, slower hunger decay
|
||||
stats.value.energy = Math.min(CONFIG.needs.energy.max, stats.value.energy + CONFIG.needs.energy.recoverPerMinuteSleep * decayFactor);
|
||||
stats.value.hunger = Math.max(0, stats.value.hunger - (CONFIG.needs.hunger.decayPerMinute * 0.5) * decayFactor);
|
||||
}
|
||||
|
||||
// Random poop generation (更低的機率:約 0.5% per tick)
|
||||
// 平均約每 200 ticks = 10 分鐘拉一次
|
||||
if (state.value !== 'sleep' && Math.random() < 0.005 && stats.value.poopCount < 4 && !isCleaning.value) {
|
||||
stats.value.poopCount++;
|
||||
}
|
||||
|
||||
// Health Logic (更溫和的健康下降)
|
||||
// 便便影響健康:每個便便每 tick -0.1 health
|
||||
// 3. Health Logic
|
||||
// Poop penalty
|
||||
if (stats.value.poopCount > 0) {
|
||||
stats.value.health = Math.max(0, stats.value.health - (0.1 * stats.value.poopCount));
|
||||
}
|
||||
|
||||
// 飢餓影響健康:飢餓值低於 20 時開始影響健康
|
||||
if (stats.value.hunger < 20) {
|
||||
const hungerPenalty = (20 - stats.value.hunger) * 0.02; // 飢餓越嚴重,扣越多
|
||||
stats.value.health = Math.max(0, stats.value.health - hungerPenalty);
|
||||
// Starvation penalty
|
||||
if (stats.value.hunger < CONFIG.needs.hunger.criticalThreshold) {
|
||||
stats.value.health = Math.max(0, stats.value.health - 0.05);
|
||||
stats.value.hungerEvents++; // Track for evolution
|
||||
}
|
||||
|
||||
// 不開心影響健康:快樂值低於 20 時開始影響健康(較輕微)
|
||||
if (stats.value.happiness < 20) {
|
||||
const happinessPenalty = (20 - stats.value.happiness) * 0.01;
|
||||
stats.value.health = Math.max(0, stats.value.health - happinessPenalty);
|
||||
// Sickness Chance
|
||||
let sickChance = CONFIG.needs.sickness.baseChancePerMinute * decayFactor;
|
||||
if (stats.value.cleanliness < CONFIG.needs.cleanliness.criticalThreshold) sickChance += CONFIG.needs.sickness.extraChanceIfDirty * decayFactor;
|
||||
if (stats.value.hunger < CONFIG.needs.hunger.criticalThreshold) sickChance += CONFIG.needs.sickness.extraChanceIfStarving * decayFactor;
|
||||
|
||||
if (Math.random() < sickChance && state.value !== 'sick') {
|
||||
state.value = 'sick';
|
||||
stats.value.sicknessEvents++;
|
||||
}
|
||||
|
||||
// Sickness Check (更低的生病機率)
|
||||
if (stats.value.health < 30 && state.value !== 'sick') {
|
||||
if (Math.random() < 0.1) { // 從 0.3 降到 0.1
|
||||
state.value = 'sick';
|
||||
// 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 (健康值可以緩慢恢復)
|
||||
// 如果沒有便便、飢餓值和快樂值都高,健康值會緩慢恢復
|
||||
if (stats.value.poopCount === 0 && stats.value.hunger > 50 && stats.value.happiness > 50 && stats.value.health < 100 && state.value !== 'sick') {
|
||||
stats.value.health = Math.min(100, stats.value.health + 0.05);
|
||||
}
|
||||
function evolveTo(newStageId) {
|
||||
console.log(`Evolving from ${stage.value} to ${newStageId}`);
|
||||
stage.value = newStageId;
|
||||
triggerState('idle', 1000); // Celebration?
|
||||
}
|
||||
|
||||
// Death Check (移除死亡機制,依照之前的討論)
|
||||
// if (stats.value.health === 0) {
|
||||
// state.value = 'dead';
|
||||
// }
|
||||
function updateMood() {
|
||||
// Determine mood based on CONFIG.mood.states
|
||||
// Priority: Angry > Sad > Happy > Neutral
|
||||
const m = CONFIG.mood.states;
|
||||
|
||||
// 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.
|
||||
// This is just internal state tracking, visual update happens in PetGame via props or events
|
||||
// For now we just log or emit if needed
|
||||
}
|
||||
|
||||
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 ---
|
||||
|
|
@ -155,7 +214,7 @@ export function usePetSystem() {
|
|||
const previousState = state.value;
|
||||
state.value = tempState;
|
||||
setTimeout(() => {
|
||||
if (state.value === tempState) { // Only revert if state hasn't changed again
|
||||
if (state.value === tempState) {
|
||||
state.value = previousState === 'sleep' ? 'idle' : 'idle';
|
||||
}
|
||||
}, duration);
|
||||
|
|
@ -163,15 +222,14 @@ export function usePetSystem() {
|
|||
|
||||
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;
|
||||
// Force evolve to baby
|
||||
evolveTo('baby');
|
||||
|
||||
// Reset stats for baby
|
||||
stats.value.hunger = CONFIG.needs.hunger.startValue;
|
||||
stats.value.happiness = CONFIG.needs.happiness.startValue;
|
||||
stats.value.energy = CONFIG.needs.energy.startValue;
|
||||
stats.value.cleanliness = CONFIG.needs.cleanliness.startValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,12 +238,15 @@ export function usePetSystem() {
|
|||
state.value = 'idle';
|
||||
isCleaning.value = false;
|
||||
stats.value = {
|
||||
hunger: 100,
|
||||
happiness: 100,
|
||||
hunger: CONFIG.needs.hunger.startValue,
|
||||
happiness: CONFIG.needs.happiness.startValue,
|
||||
cleanliness: CONFIG.needs.cleanliness.startValue,
|
||||
energy: CONFIG.needs.energy.startValue,
|
||||
health: 100,
|
||||
weight: 500,
|
||||
age: 0,
|
||||
poopCount: 0
|
||||
poopCount: 0,
|
||||
ageMinutes: 0,
|
||||
hungerEvents: 0,
|
||||
sicknessEvents: 0
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,255 +2,323 @@
|
|||
// 定義各種寵物的像素藝術數據
|
||||
|
||||
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: {
|
||||
name: '小虎斑貓',
|
||||
pixelSize: 3,
|
||||
sprite: [
|
||||
'0000000000000000',
|
||||
'0011000000110000', // row 1 - Ears
|
||||
'0124444111442100', // row 2 粉紅耳朵內側
|
||||
'0123222323221000', // row 3 三條虎紋
|
||||
'0122322223221000', // row 4 - Stripes
|
||||
'0122522222522100', // row 5 眼睛反光
|
||||
'0125052225052100', // row 6 大圓眼+黑瞳孔+白反光
|
||||
'0112223322221100', // row 7 鼻子+左右鬍鬚
|
||||
'0122220222221000', // 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',
|
||||
'0124444111442100',
|
||||
'0123222323221000',
|
||||
'0122322223221000',
|
||||
'0122522222522100',
|
||||
'0125052225052100',
|
||||
'0112223322221100',
|
||||
'0122204002221000', // 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
|
||||
'4': '#ffb6c1', // Pink (ears, blush, tongue)
|
||||
'5': '#ffffff' // White eye highlight
|
||||
id: 'tinyTigerCatB',
|
||||
meta: {
|
||||
name: '小虎斑貓',
|
||||
displayNameEn: 'Tiny Tiger Cat',
|
||||
species: 'cat',
|
||||
element: 'normal',
|
||||
description: '一隻活潑、黏人的小虎斑貓,喜歡被餵食和玩耍。'
|
||||
},
|
||||
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]
|
||||
],
|
||||
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 },
|
||||
iconBackRight: { x: 13, y: 2 },
|
||||
|
||||
// Growth Stages
|
||||
eggSprite: [
|
||||
'0000000000000000',
|
||||
'0000000000000000',
|
||||
'0000000111000000', // Top (Narrow)
|
||||
'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
|
||||
}
|
||||
lifecycle: {
|
||||
baseLifeMinutes: 7 * 24 * 60,
|
||||
stages: [
|
||||
{
|
||||
id: 'egg',
|
||||
name: '蛋',
|
||||
minAgeMinutes: 0,
|
||||
maxAgeMinutes: 30,
|
||||
spriteKey: 'egg',
|
||||
canBattle: false,
|
||||
canEquip: false
|
||||
},
|
||||
{
|
||||
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
|
||||
}
|
||||
],
|
||||
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',
|
||||
'0011000000110000',
|
||||
'0124444111442100',
|
||||
'0123222323221000',
|
||||
'0122322223221000',
|
||||
'0122522222522100',
|
||||
'0125052225052100',
|
||||
'0112223322221100',
|
||||
'0122220222221000',
|
||||
'0011222222110000',
|
||||
'0001222222121000',
|
||||
'0001222222121000',
|
||||
'0001100110110000',
|
||||
'0000000000000000',
|
||||
'0000000000000000',
|
||||
'0000000000000000',
|
||||
],
|
||||
mouthOpen: [
|
||||
'0000000000000000',
|
||||
'0011000000110000',
|
||||
'0124444111442100',
|
||||
'0123222323221000',
|
||||
'0122322223221000',
|
||||
'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: [
|
||||
[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]
|
||||
],
|
||||
eyePixels: [
|
||||
[3, 6], [4, 6],
|
||||
[8, 6], [9, 6]
|
||||
],
|
||||
iconBackLeft: { x: 2, y: 2 },
|
||||
iconBackRight: { x: 13, y: 2 }
|
||||
},
|
||||
behaviorAnimation: {
|
||||
blinkIntervalSec: 5,
|
||||
blinkDurationMs: 200,
|
||||
mouthOpenDurationMs: 300,
|
||||
idleEmoteIntervalSec: 15
|
||||
}
|
||||
},
|
||||
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