Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
bae581b778 |
148
src/App.vue
148
src/App.vue
|
|
@ -5,15 +5,21 @@ import DeviceScreen from './components/DeviceScreen.vue';
|
||||||
import PetGame from './components/PetGame.vue';
|
import PetGame from './components/PetGame.vue';
|
||||||
import Menu from './components/Menu.vue';
|
import Menu from './components/Menu.vue';
|
||||||
import { usePetSystem } from './composables/usePetSystem';
|
import { usePetSystem } from './composables/usePetSystem';
|
||||||
import { useEventSystem } from './composables/useEventSystem';
|
|
||||||
|
|
||||||
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'
|
||||||
const debugAction = ref(null); // For passing debug commands to PetGame
|
|
||||||
|
// 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 petSystem = usePetSystem();
|
|
||||||
const {
|
const {
|
||||||
stage,
|
stage,
|
||||||
state,
|
state,
|
||||||
|
|
@ -24,24 +30,51 @@ const {
|
||||||
clean,
|
clean,
|
||||||
isCleaning,
|
isCleaning,
|
||||||
hatchEgg,
|
hatchEgg,
|
||||||
reset,
|
reset
|
||||||
achievements,
|
} = usePetSystem();
|
||||||
unlockAllAchievements,
|
|
||||||
resurrect,
|
|
||||||
reincarnate
|
|
||||||
} = petSystem;
|
|
||||||
|
|
||||||
// Initialize Event System
|
// Handle Physical Buttons
|
||||||
const { currentEvent, checkEventTriggers, triggerRandomEvent } = useEventSystem(petSystem);
|
function handleButton(btnId) {
|
||||||
|
console.log('Button pressed:', btnId);
|
||||||
|
|
||||||
// Start Event Loop
|
// 0. Check if PetGame wants to handle the input (e.g. Prayer Menu, Minigames)
|
||||||
setInterval(() => {
|
if (petGameRef.value && petGameRef.value.handleInput) {
|
||||||
checkEventTriggers();
|
if (petGameRef.value.handleInput(btnId)) {
|
||||||
}, 10000); // Check every 10 seconds
|
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();
|
||||||
|
|
@ -54,34 +87,38 @@ function handleAction(action) {
|
||||||
clean();
|
clean();
|
||||||
break;
|
break;
|
||||||
case 'play':
|
case 'play':
|
||||||
if (play()) {
|
play();
|
||||||
if (petGameRef.value) {
|
|
||||||
petGameRef.value.startPlaying();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'sleep':
|
case 'sleep':
|
||||||
sleep();
|
sleep();
|
||||||
break;
|
break;
|
||||||
case 'medicine':
|
case 'medicine':
|
||||||
// Heal the pet with animation
|
// Heal the pet
|
||||||
if (state.value === 'sick') {
|
if (state.value === 'sick') {
|
||||||
if (petGameRef.value) {
|
|
||||||
// Trigger medicine animation
|
|
||||||
petGameRef.value.startFeeding('medicine').then(() => {
|
|
||||||
stats.value.health = 100;
|
stats.value.health = 100;
|
||||||
state.value = 'idle';
|
state.value = 'idle';
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Fallback if ref not ready
|
|
||||||
stats.value.health = 100;
|
|
||||||
state.value = 'idle';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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
|
||||||
|
|
@ -91,22 +128,6 @@ function handleAction(action) {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'jiaobei':
|
|
||||||
// 擲筊功能 - 待實作
|
|
||||||
console.log('擲筊功能');
|
|
||||||
// TODO: 實作擲筊邏輯
|
|
||||||
break;
|
|
||||||
case 'fortune':
|
|
||||||
// 求籤功能 - 待實作
|
|
||||||
console.log('求籤功能');
|
|
||||||
// TODO: 實作求籤邏輯
|
|
||||||
break;
|
|
||||||
case 'resurrect':
|
|
||||||
resurrect();
|
|
||||||
break;
|
|
||||||
case 'reincarnate':
|
|
||||||
reincarnate();
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
console.log('Action not implemented:', action);
|
console.log('Action not implemented:', action);
|
||||||
}
|
}
|
||||||
|
|
@ -116,14 +137,10 @@ function handleAction(action) {
|
||||||
function setPetState(newState) {
|
function setPetState(newState) {
|
||||||
state.value = newState;
|
state.value = newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerDebugAction(action, payload = null) {
|
|
||||||
debugAction.value = { type: action, payload, timestamp: Date.now() };
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DeviceShell>
|
<DeviceShell @btn1="handleButton(1)" @btn2="handleButton(2)" @btn3="handleButton(3)">
|
||||||
<DeviceScreen>
|
<DeviceScreen>
|
||||||
<!-- Dynamic Component Switching -->
|
<!-- Dynamic Component Switching -->
|
||||||
<PetGame
|
<PetGame
|
||||||
|
|
@ -133,10 +150,9 @@ function triggerDebugAction(action, payload = null) {
|
||||||
:stage="stage"
|
:stage="stage"
|
||||||
:stats="stats"
|
:stats="stats"
|
||||||
:isCleaning="isCleaning"
|
:isCleaning="isCleaning"
|
||||||
:showStats="showStats"
|
:statsMode="statsMode"
|
||||||
:debugAction="debugAction"
|
:isMenuOpen="isMenuOpen"
|
||||||
:achievements="achievements"
|
:selectedMenuIndex="selectedMenuIndex"
|
||||||
:currentEvent="currentEvent"
|
|
||||||
@update:state="state = $event"
|
@update:state="state = $event"
|
||||||
@action="handleAction"
|
@action="handleAction"
|
||||||
/>
|
/>
|
||||||
|
|
@ -162,22 +178,6 @@ function triggerDebugAction(action, payload = null) {
|
||||||
<button v-if="stage === 'egg'" @click="hatchEgg()">🥚 Hatch Egg</button>
|
<button v-if="stage === 'egg'" @click="hatchEgg()">🥚 Hatch Egg</button>
|
||||||
<button v-else @click="reset()">🔄 Reset to Egg</button>
|
<button v-else @click="reset()">🔄 Reset to Egg</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group">
|
|
||||||
<button @click="triggerDebugAction('randomEvent')">🎲 Random Event</button>
|
|
||||||
<button @click="triggerDebugAction('addItem', 'sunglasses')">🕶️ Add Sunglasses</button>
|
|
||||||
<button @click="triggerDebugAction('addItem', 'cookie')">🍪 Add Cookie</button>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group">
|
|
||||||
<button @click="triggerDebugAction('setMood', 'happy')">😊 Happy</button>
|
|
||||||
<button @click="triggerDebugAction('setMood', 'angry')">😠 Angry</button>
|
|
||||||
<button @click="triggerDebugAction('setMood', 'sad')">😢 Sad</button>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group">
|
|
||||||
<button @click="unlockAllAchievements()">🏆 Unlock All Achievements</button>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group">
|
|
||||||
<button @click="stats.dailyPrayerCount = 0">🙏 Reset Prayer Count</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -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-sleep" @click="$emit('sleep')" :disabled="disabled" title="關燈"></button>
|
:class="{ active: isMenuOpen && selectedIndex === 4 }"
|
||||||
<button class="icon-btn icon-backpack" @click="$emit('inventory')" :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,10 +48,18 @@ const props = defineProps({
|
||||||
isSick: {
|
isSick: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
isMenuOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
selectedIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(['clean', 'medicine', 'sleep', 'inventory']);
|
defineEmits(['clean', 'medicine', 'training', 'info']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -48,10 +80,20 @@ defineEmits(['clean', 'medicine', 'sleep', 'inventory']);
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,54 +132,45 @@ defineEmits(['clean', 'medicine', 'sleep', 'inventory']);
|
||||||
0px 4px 0 #ff4444, 0px 6px 0 #ff4444;
|
0px 4px 0 #ff4444, 0px 6px 0 #ff4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sleep Icon (Light Bulb/燈泡) - Enhanced */
|
/* Training Icon (Praying Hands) */
|
||||||
.icon-sleep::before {
|
.icon-training::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: transparent;
|
background: #d4a574; /* 手的膚色 */
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
/* 燈泡主體 (圓形) */
|
/* 光芒 - 頂部 */
|
||||||
-2px -6px 0 #ffd700, 0px -6px 0 #ffd700, 2px -6px 0 #ffd700,
|
0px -8px 0 #ffcc00,
|
||||||
-4px -4px 0 #ffd700, -2px -4px 0 #ffd700, 0px -4px 0 #ffd700, 2px -4px 0 #ffd700, 4px -4px 0 #ffd700,
|
-2px -6px 0 #ffcc00, 2px -6px 0 #ffcc00,
|
||||||
-4px -2px 0 #ffd700, -2px -2px 0 #ffd700, 0px -2px 0 #ffd700, 2px -2px 0 #ffd700, 4px -2px 0 #ffd700,
|
|
||||||
-4px 0px 0 #ffd700, -2px 0px 0 #ffd700, 0px 0px 0 #ffd700, 2px 0px 0 #ffd700, 4px 0px 0 #ffd700,
|
/* 合掌的手 - 簡化版 */
|
||||||
-2px 2px 0 #ffd700, 0px 2px 0 #ffd700, 2px 2px 0 #ffd700,
|
-2px -4px 0 #d4a574, 0px -4px 0 #d4a574, 2px -4px 0 #d4a574,
|
||||||
/* 燈泡底部 (螺旋) */
|
-2px -2px 0 #d4a574, 0px -2px 0 #d4a574, 2px -2px 0 #d4a574,
|
||||||
-1px 4px 0 #8B4513, 0px 4px 0 #8B4513, 1px 4px 0 #8B4513,
|
-2px 0px 0 #d4a574, 0px 0px 0 #d4a574, 2px 0px 0 #d4a574,
|
||||||
/* 光線 (向下) */
|
0px 2px 0 #d4a574, 0px 4px 0 #d4a574,
|
||||||
0px 6px 0 #ffd700, 0px 8px 0 #ffd700;
|
|
||||||
|
/* 光芒 - 左右 */
|
||||||
|
-6px -2px 0 #ffcc00, 6px -2px 0 #ffcc00,
|
||||||
|
-6px 0px 0 #ffcc00, 6px 0px 0 #ffcc00;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Backpack Icon */
|
/* Info Icon (i) */
|
||||||
.icon-backpack::before {
|
.icon-info::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: #8d6e63;
|
background: #4444ff;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
/* Top flap */
|
0px -6px 0 #4444ff,
|
||||||
-2px -6px 0 #8d6e63, 0px -6px 0 #8d6e63, 2px -6px 0 #8d6e63,
|
0px -2px 0 #4444ff, 0px 0px 0 #4444ff,
|
||||||
-4px -4px 0 #8d6e63, -2px -4px 0 #a1887f, 0px -4px 0 #a1887f, 2px -4px 0 #a1887f, 4px -4px 0 #8d6e63,
|
0px 2px 0 #4444ff, 0px 4px 0 #4444ff, 0px 6px 0 #4444ff;
|
||||||
|
|
||||||
/* Body */
|
|
||||||
-4px -2px 0 #8d6e63, -2px -2px 0 #5d4037, 0px -2px 0 #5d4037, 2px -2px 0 #5d4037, 4px -2px 0 #8d6e63,
|
|
||||||
-4px 0px 0 #8d6e63, -2px 0px 0 #5d4037, 0px 0px 0 #5d4037, 2px 0px 0 #5d4037, 4px 0px 0 #8d6e63,
|
|
||||||
-4px 2px 0 #8d6e63, -2px 2px 0 #5d4037, 0px 2px 0 #5d4037, 2px 2px 0 #5d4037, 4px 2px 0 #8d6e63,
|
|
||||||
-4px 4px 0 #8d6e63, -2px 4px 0 #5d4037, 0px 4px 0 #5d4037, 2px 4px 0 #5d4037, 4px 4px 0 #8d6e63,
|
|
||||||
|
|
||||||
/* Bottom */
|
|
||||||
-2px 6px 0 #8d6e63, 0px 6px 0 #8d6e63, 2px 6px 0 #8d6e63;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,307 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="ball-game-layer" @touchstart="handleTouch" @mousedown="handleMouse">
|
|
||||||
<canvas ref="gameCanvas" class="game-canvas"></canvas>
|
|
||||||
|
|
||||||
<div class="game-ui">
|
|
||||||
<div class="score">接球: {{ score }}/10</div>
|
|
||||||
|
|
||||||
<div v-if="!gameStarted" class="start-hint">
|
|
||||||
<p>點擊左右移動接球!</p>
|
|
||||||
<p>點擊螢幕開始</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="gameOver" class="game-over-msg">
|
|
||||||
<p>{{ win ? '大成功!' : '再試一次' }}</p>
|
|
||||||
<button @click.stop="handleClose">完成</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 觸控區域提示 (僅在開始前顯示) -->
|
|
||||||
<div v-if="!gameStarted" class="touch-zones">
|
|
||||||
<div class="zone left">← 左</div>
|
|
||||||
<div class="zone right">右 →</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'complete', 'updatePetX']);
|
|
||||||
|
|
||||||
const gameCanvas = ref(null);
|
|
||||||
const gameStarted = ref(false);
|
|
||||||
const gameOver = ref(false);
|
|
||||||
const win = ref(false);
|
|
||||||
const score = ref(0);
|
|
||||||
const targetScore = 10;
|
|
||||||
|
|
||||||
// 遊戲數據
|
|
||||||
let balls = [];
|
|
||||||
let animationId = null;
|
|
||||||
let ctx = null;
|
|
||||||
let canvasWidth = 300;
|
|
||||||
let canvasHeight = 150;
|
|
||||||
let lastTime = 0;
|
|
||||||
let spawnTimer = 0;
|
|
||||||
|
|
||||||
// 寵物數據 (需要與 PetGame 同步)
|
|
||||||
let petX = 150;
|
|
||||||
const PET_Y = 100; // 假設寵物在底部
|
|
||||||
const PET_WIDTH = 32;
|
|
||||||
const PET_SPEED = 15; // 移動速度
|
|
||||||
|
|
||||||
function initGame() {
|
|
||||||
if (!gameCanvas.value) return;
|
|
||||||
const canvas = gameCanvas.value;
|
|
||||||
const rect = canvas.parentElement.getBoundingClientRect();
|
|
||||||
canvas.width = rect.width;
|
|
||||||
canvas.height = rect.height;
|
|
||||||
canvasWidth = rect.width;
|
|
||||||
canvasHeight = rect.height;
|
|
||||||
|
|
||||||
// 初始寵物位置在中間
|
|
||||||
petX = canvasWidth / 2 - PET_WIDTH / 2;
|
|
||||||
emit('updatePetX', petX);
|
|
||||||
|
|
||||||
ctx = canvas.getContext('2d');
|
|
||||||
loop(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function startGame() {
|
|
||||||
gameStarted.value = true;
|
|
||||||
gameOver.value = false;
|
|
||||||
score.value = 0;
|
|
||||||
balls = [];
|
|
||||||
lastTime = performance.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTouch(e) {
|
|
||||||
if (gameOver.value) return;
|
|
||||||
if (!gameStarted.value) {
|
|
||||||
startGame();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const touchX = e.touches[0].clientX;
|
|
||||||
const rect = gameCanvas.value.getBoundingClientRect();
|
|
||||||
const relativeX = touchX - rect.left;
|
|
||||||
|
|
||||||
movePet(relativeX < rect.width / 2 ? -1 : 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMouse(e) {
|
|
||||||
if (gameOver.value) return;
|
|
||||||
if (!gameStarted.value) {
|
|
||||||
startGame();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = gameCanvas.value.getBoundingClientRect();
|
|
||||||
const relativeX = e.clientX - rect.left;
|
|
||||||
|
|
||||||
movePet(relativeX < rect.width / 2 ? -1 : 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function movePet(direction) {
|
|
||||||
petX += direction * PET_SPEED;
|
|
||||||
// 邊界檢查
|
|
||||||
petX = Math.max(0, Math.min(petX, canvasWidth - PET_WIDTH));
|
|
||||||
emit('updatePetX', petX);
|
|
||||||
}
|
|
||||||
|
|
||||||
function spawnBall() {
|
|
||||||
const size = 12;
|
|
||||||
balls.push({
|
|
||||||
x: Math.random() * (canvasWidth - size),
|
|
||||||
y: -20,
|
|
||||||
speed: 2 + Math.random() * 2, // 隨機速度
|
|
||||||
size: size,
|
|
||||||
color: Math.random() > 0.8 ? '#ffd700' : '#ff4444' // 金球或紅球
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loop(timestamp) {
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
const deltaTime = timestamp - lastTime;
|
|
||||||
lastTime = timestamp;
|
|
||||||
|
|
||||||
// 清空畫布
|
|
||||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
|
||||||
|
|
||||||
if (gameStarted.value && !gameOver.value) {
|
|
||||||
update(deltaTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
draw();
|
|
||||||
|
|
||||||
animationId = requestAnimationFrame(loop);
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(deltaTime) {
|
|
||||||
// 生成球
|
|
||||||
spawnTimer += deltaTime;
|
|
||||||
if (spawnTimer > 1000) { // 每秒一顆
|
|
||||||
spawnBall();
|
|
||||||
spawnTimer = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新球的位置
|
|
||||||
for (let i = balls.length - 1; i >= 0; i--) {
|
|
||||||
const ball = balls[i];
|
|
||||||
ball.y += ball.speed;
|
|
||||||
|
|
||||||
// 碰撞檢測 (接球)
|
|
||||||
// 假設寵物判定框
|
|
||||||
if (ball.y + ball.size > PET_Y &&
|
|
||||||
ball.y < PET_Y + 32 &&
|
|
||||||
ball.x + ball.size > petX &&
|
|
||||||
ball.x < petX + PET_WIDTH) {
|
|
||||||
|
|
||||||
// 接到了!
|
|
||||||
score.value++;
|
|
||||||
balls.splice(i, 1);
|
|
||||||
|
|
||||||
// 檢查勝利
|
|
||||||
if (score.value >= targetScore) {
|
|
||||||
gameOver.value = true;
|
|
||||||
win.value = true;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 掉出底部
|
|
||||||
if (ball.y > canvasHeight) {
|
|
||||||
balls.splice(i, 1);
|
|
||||||
// 可以扣分或失敗,這裡暫時不處罰,輕鬆一點
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
// 繪製球
|
|
||||||
for (const ball of balls) {
|
|
||||||
drawPixelBall(ball);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawPixelBall(ball) {
|
|
||||||
const { x, y, size, color } = ball;
|
|
||||||
const pSize = 4; // 像素大小
|
|
||||||
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
// 簡單的 3x3 像素球
|
|
||||||
// X
|
|
||||||
// XXX
|
|
||||||
// X
|
|
||||||
ctx.fillRect(x + pSize, y, pSize, pSize);
|
|
||||||
ctx.fillRect(x, y + pSize, pSize * 3, pSize);
|
|
||||||
ctx.fillRect(x + pSize, y + pSize * 2, pSize, pSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClose() {
|
|
||||||
emit('complete', win.value);
|
|
||||||
emit('close');
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initGame();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (animationId) cancelAnimationFrame(animationId);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.ball-game-layer {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 50;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-canvas {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-ui {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score {
|
|
||||||
font-family: 'DotGothic16', sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
color: #8b4513;
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-hint {
|
|
||||||
margin-top: 40px;
|
|
||||||
font-family: 'DotGothic16', sans-serif;
|
|
||||||
color: #8b4513;
|
|
||||||
text-align: center;
|
|
||||||
animation: pulse 1s infinite;
|
|
||||||
background: rgba(255, 255, 255, 0.5);
|
|
||||||
padding: 5px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-over-msg {
|
|
||||||
margin-top: 40px;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
padding: 10px;
|
|
||||||
border: 2px solid #8b4513;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-align: center;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-over-msg button {
|
|
||||||
margin-top: 5px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
background: #4CAF50;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.touch-zones {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zone {
|
|
||||||
font-family: 'DotGothic16', sans-serif;
|
|
||||||
color: #8b4513;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { opacity: 0.6; }
|
|
||||||
50% { opacity: 1; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,637 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="deity-temple-overlay">
|
|
||||||
<div class="temple-container" @click.stop>
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="temple-header">
|
|
||||||
<button class="close-btn" @click="$emit('close')">×</button>
|
|
||||||
<h2 class="temple-title">神廟</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scrollable Content -->
|
|
||||||
<div class="temple-content">
|
|
||||||
<!-- Deity Display -->
|
|
||||||
<div class="deity-display">
|
|
||||||
<div class="deity-icon" :class="currentDeity.icon"></div>
|
|
||||||
<div class="deity-name">{{ currentDeity.name }}</div>
|
|
||||||
<div class="deity-subtitle">{{ currentDeity.personality }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Dialogue Bubble -->
|
|
||||||
<div class="dialogue-bubble">
|
|
||||||
<p>{{ currentDialogue }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Favor Progress -->
|
|
||||||
<div class="favor-section">
|
|
||||||
<div class="favor-label">
|
|
||||||
好感度: {{ favorStars }} ({{ currentFavor }}/100)
|
|
||||||
</div>
|
|
||||||
<div class="favor-bar">
|
|
||||||
<div class="favor-fill" :style="{ width: currentFavor + '%' }"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Active Buffs -->
|
|
||||||
<div class="buffs-section" v-if="activeBuffs.length > 0">
|
|
||||||
<div class="buff-title">當前加成:</div>
|
|
||||||
<div class="buff-list">
|
|
||||||
<div v-for="buff in activeBuffs" :key="buff.type" class="buff-item">
|
|
||||||
• {{ buff.description }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Prayer Options -->
|
|
||||||
<div class="prayer-section">
|
|
||||||
<!-- 祈福按鈕 -->
|
|
||||||
<button
|
|
||||||
class="prayer-btn"
|
|
||||||
:disabled="dailyPrayerCount >= 3"
|
|
||||||
@click="handlePrayer"
|
|
||||||
>
|
|
||||||
🙏 祈福 (今日 {{ dailyPrayerCount }}/3)
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="prayer-divider">或</div>
|
|
||||||
|
|
||||||
<div class="prayer-options">
|
|
||||||
<!-- 擲筊選項 -->
|
|
||||||
<button
|
|
||||||
class="prayer-option"
|
|
||||||
@click="handlePrayerSelect('jiaobei')"
|
|
||||||
>
|
|
||||||
<div class="option-icon icon-jiaobei"></div>
|
|
||||||
<span class="option-label">擲筊</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- 求籤選項 -->
|
|
||||||
<button
|
|
||||||
class="prayer-option"
|
|
||||||
@click="handlePrayerSelect('fortune')"
|
|
||||||
>
|
|
||||||
<div class="option-icon icon-fortune"></div>
|
|
||||||
<span class="option-label">求籤</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bottom Actions -->
|
|
||||||
<div class="bottom-actions">
|
|
||||||
<button class="action-btn" @click="showDeitySelector = !showDeitySelector">
|
|
||||||
切換神明
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Deity Selector (Overlay on top of content) -->
|
|
||||||
<div v-if="showDeitySelector" class="deity-selector-overlay" @click="showDeitySelector = false">
|
|
||||||
<div class="deity-selector-menu" @click.stop>
|
|
||||||
<div class="selector-header">選擇神明</div>
|
|
||||||
<div
|
|
||||||
v-for="deity in DEITIES"
|
|
||||||
:key="deity.id"
|
|
||||||
class="deity-option"
|
|
||||||
:class="{ active: deity.id === currentDeity.id }"
|
|
||||||
@click="selectDeity(deity.id)"
|
|
||||||
>
|
|
||||||
<div class="deity-icon-small" :class="deity.icon"></div>
|
|
||||||
<span>{{ deity.name }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
deityFavors: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
currentDeityId: {
|
|
||||||
type: String,
|
|
||||||
default: 'mazu'
|
|
||||||
},
|
|
||||||
dailyPrayerCount: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'prayer', 'change-deity', 'prayer-select']);
|
|
||||||
|
|
||||||
// Deity Data
|
|
||||||
const DEITIES = [
|
|
||||||
{
|
|
||||||
id: 'mazu',
|
|
||||||
name: '媽祖',
|
|
||||||
personality: '溫柔守護',
|
|
||||||
buffs: {
|
|
||||||
gameSuccessRate: 0.1,
|
|
||||||
sicknessReduction: 0.15
|
|
||||||
},
|
|
||||||
buffDescriptions: [
|
|
||||||
'小遊戲成功率 +10%',
|
|
||||||
'生病機率 -15%'
|
|
||||||
],
|
|
||||||
dialogues: [
|
|
||||||
"好孩子,媽祖保佑你平安喔",
|
|
||||||
"海上無風浪,心中有媽祖",
|
|
||||||
"要好好照顧寵物啊"
|
|
||||||
],
|
|
||||||
icon: 'deity-mazu'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'earthgod',
|
|
||||||
name: '土地公',
|
|
||||||
personality: '碎念管家',
|
|
||||||
buffs: {
|
|
||||||
itemDropRate: 0.2,
|
|
||||||
resourceGain: 0.15
|
|
||||||
},
|
|
||||||
buffDescriptions: [
|
|
||||||
'掉落物品機率 +20%',
|
|
||||||
'資源獲得 +15%'
|
|
||||||
],
|
|
||||||
dialogues: [
|
|
||||||
"又來啦?今天有好好餵寵物嗎?",
|
|
||||||
"欸,地上那個便便怎麼不清一清",
|
|
||||||
"拜我就對了,土地公最靈驗"
|
|
||||||
],
|
|
||||||
icon: 'deity-earthgod'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'matchmaker',
|
|
||||||
name: '月老',
|
|
||||||
personality: '八卦熱情',
|
|
||||||
buffs: {
|
|
||||||
happinessRecovery: 0.25,
|
|
||||||
goodEventRate: 0.1
|
|
||||||
},
|
|
||||||
buffDescriptions: [
|
|
||||||
'Happiness 回復 +25%',
|
|
||||||
'好事件機率 +10%'
|
|
||||||
],
|
|
||||||
dialogues: [
|
|
||||||
"哎呀~你的寵物今天心情不錯喔",
|
|
||||||
"要不要幫你牽條紅線?咦,寵物也需要嗎",
|
|
||||||
"姻緣天注定,開心最重要!"
|
|
||||||
],
|
|
||||||
icon: 'deity-matchmaker'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'wenchang',
|
|
||||||
name: '文昌帝君',
|
|
||||||
personality: '嚴肅學者',
|
|
||||||
buffs: {
|
|
||||||
intGrowth: 0.3,
|
|
||||||
guessingReward: 0.2
|
|
||||||
},
|
|
||||||
buffDescriptions: [
|
|
||||||
'INT 成長 +30%',
|
|
||||||
'猜拳獎勵 +20%'
|
|
||||||
],
|
|
||||||
dialogues: [
|
|
||||||
"學海無涯,勤能補拙",
|
|
||||||
"多動腦,少偷懶",
|
|
||||||
"智慧是一切的根本"
|
|
||||||
],
|
|
||||||
icon: 'deity-wenchang'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'guanyin',
|
|
||||||
name: '觀音菩薩',
|
|
||||||
personality: '慈悲救苦',
|
|
||||||
buffs: {
|
|
||||||
healthRecovery: 0.2,
|
|
||||||
autoHeal: true
|
|
||||||
},
|
|
||||||
buffDescriptions: [
|
|
||||||
'Health 回復 +20%',
|
|
||||||
'自動治療 (1次/天)'
|
|
||||||
],
|
|
||||||
dialogues: [
|
|
||||||
"阿彌陀佛,施主請安心",
|
|
||||||
"救苦救難,觀音保佑",
|
|
||||||
"..."
|
|
||||||
],
|
|
||||||
icon: 'deity-guanyin'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const showDeitySelector = ref(false);
|
|
||||||
const currentDialogue = ref('');
|
|
||||||
const activeBuffs = ref([]);
|
|
||||||
|
|
||||||
const currentDeity = computed(() => {
|
|
||||||
return DEITIES.find(d => d.id === props.currentDeityId) || DEITIES[0];
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentFavor = computed(() => {
|
|
||||||
return props.deityFavors[props.currentDeityId] || 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const favorStars = computed(() => {
|
|
||||||
const level = Math.floor(currentFavor.value / 20);
|
|
||||||
return '★'.repeat(level) + '☆'.repeat(5 - level);
|
|
||||||
});
|
|
||||||
|
|
||||||
function selectDeity(deityId) {
|
|
||||||
emit('change-deity', deityId);
|
|
||||||
showDeitySelector.value = false;
|
|
||||||
updateDialogue();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateDialogue() {
|
|
||||||
const dialogues = currentDeity.value.dialogues;
|
|
||||||
currentDialogue.value = dialogues[Math.floor(Math.random() * dialogues.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePrayer() {
|
|
||||||
if (props.dailyPrayerCount >= 3) return;
|
|
||||||
|
|
||||||
emit('prayer', currentDeity.value.id);
|
|
||||||
updateDialogue();
|
|
||||||
|
|
||||||
// Update active buffs display
|
|
||||||
activeBuffs.value = currentDeity.value.buffDescriptions.map(desc => ({
|
|
||||||
type: currentDeity.value.id,
|
|
||||||
description: desc
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePrayerSelect(mode) {
|
|
||||||
emit('prayer-select', mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
updateDialogue();
|
|
||||||
|
|
||||||
// Load active buffs if favor > 0
|
|
||||||
if (currentFavor.value > 0) {
|
|
||||||
activeBuffs.value = currentDeity.value.buffDescriptions.map(desc => ({
|
|
||||||
type: currentDeity.value.id,
|
|
||||||
description: desc
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.deity-temple-overlay {
|
|
||||||
position: absolute; /* Absolute to parent (DeviceScreen) */
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.85);
|
|
||||||
z-index: 50; /* Same as PetInfoScreen */
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.temple-container {
|
|
||||||
background: linear-gradient(to bottom, #8B4513 0%, #A0522D 100%);
|
|
||||||
border: 3px solid #654321;
|
|
||||||
border-radius: 6px;
|
|
||||||
width: calc(100% - 8px);
|
|
||||||
max-width: 198px; /* Fit within 210px screen with padding */
|
|
||||||
height: calc(100% - 8px);
|
|
||||||
max-height: 152px; /* Fit within 160px screen with padding */
|
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden; /* Contain children */
|
|
||||||
pointer-events: auto; /* Ensure container can receive clicks */
|
|
||||||
}
|
|
||||||
|
|
||||||
.temple-container::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.temple-container::-webkit-scrollbar-track {
|
|
||||||
background: rgba(0,0,0,0.2);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.temple-container::-webkit-scrollbar-thumb {
|
|
||||||
background: #FFD700;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.temple-header {
|
|
||||||
background: #654321;
|
|
||||||
padding: 6px;
|
|
||||||
text-align: center;
|
|
||||||
border-bottom: 2px solid #4a3216;
|
|
||||||
flex-shrink: 0; /* Don't shrink header */
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.temple-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto; /* Scrollable content area */
|
|
||||||
overflow-x: hidden;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
position: absolute;
|
|
||||||
right: 4px;
|
|
||||||
top: 4px;
|
|
||||||
background: #8B0000;
|
|
||||||
border: 2px solid #fff;
|
|
||||||
color: #fff;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1;
|
|
||||||
z-index: 10;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.temple-title {
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
color: #FFD700;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deity-display { padding: 6px; text-align: center; }
|
|
||||||
.deity-icon { width: 32px; height: 32px; margin: 0 auto 4px; background: #FFD700; border: 2px solid #FFA500; border-radius: 50%; }
|
|
||||||
.deity-name { font-family: 'DotGothic16', monospace; font-size: 11px; color: #FFD700; font-weight: bold; margin-bottom: 2px; }
|
|
||||||
.deity-subtitle { font-family: 'DotGothic16', monospace; font-size: 9px; color: #DEB887; }
|
|
||||||
|
|
||||||
.dialogue-bubble {
|
|
||||||
margin: 0 6px 6px;
|
|
||||||
background: #FFF8DC;
|
|
||||||
border: 2px solid #654321;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 6px;
|
|
||||||
position: relative;
|
|
||||||
min-height: 30px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.dialogue-bubble::before {
|
|
||||||
content: ''; position: absolute; top: -6px; left: 50%; transform: translateX(-50%);
|
|
||||||
border-width: 0 6px 6px 6px; border-style: solid; border-color: transparent transparent #654321 transparent;
|
|
||||||
}
|
|
||||||
.dialogue-bubble p { font-family: 'DotGothic16', monospace; font-size: 9px; color: #333; margin: 0; text-align: center; line-height: 1.3; }
|
|
||||||
|
|
||||||
.favor-section, .buffs-section { padding: 0 6px 6px; }
|
|
||||||
.prayer-section {
|
|
||||||
padding: 0 6px 6px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 5;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prayer-btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: 6px;
|
|
||||||
background: #FFD700;
|
|
||||||
border: 2px solid #FFA500;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #8B4513;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
}
|
|
||||||
.prayer-btn:hover:not(:disabled) {
|
|
||||||
background: #FFA500;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
.prayer-btn:active:not(:disabled) {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
.prayer-btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
background: #ccc;
|
|
||||||
border-color: #999;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prayer-divider {
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 8px;
|
|
||||||
color: #DEB887;
|
|
||||||
text-align: center;
|
|
||||||
margin: 2px 0;
|
|
||||||
}
|
|
||||||
.bottom-actions {
|
|
||||||
padding: 4px 6px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-top: 2px solid #4a3216;
|
|
||||||
background: rgba(101, 67, 33, 0.5);
|
|
||||||
position: relative;
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
.favor-label { font-family: 'DotGothic16', monospace; font-size: 9px; color: #FFD700; margin-bottom: 2px; }
|
|
||||||
.favor-bar { height: 8px; background: rgba(0,0,0,0.3); border: 1px solid #654321; border-radius: 4px; overflow: hidden; }
|
|
||||||
.favor-fill { height: 100%; background: linear-gradient(to right, #FFD700, #FFA500); transition: width 0.3s ease; }
|
|
||||||
|
|
||||||
.buffs-section { background: rgba(255,255,255,0.1); border-radius: 3px; padding: 4px; }
|
|
||||||
.buff-title { font-family: 'DotGothic16', monospace; font-size: 9px; color: #FFD700; margin-bottom: 2px; }
|
|
||||||
.buff-list { font-family: 'DotGothic16', monospace; font-size: 8px; color: #FFF8DC; }
|
|
||||||
|
|
||||||
.prayer-options {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prayer-option {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2px;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
border: 2px solid #654321;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 4px 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
|
||||||
min-width: 50px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prayer-option:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
||||||
background: #FFD700;
|
|
||||||
border-color: #FFA500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prayer-option:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-icon {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
position: relative;
|
|
||||||
transform: scale(0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-label {
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 8px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prayer-count-info {
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 8px;
|
|
||||||
color: #FFD700;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
width: 100%; padding: 6px; background: #654321; border: 2px solid #4a3216; border-radius: 3px;
|
|
||||||
font-family: 'DotGothic16', monospace; font-size: 9px; color: #FFD700; cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
}
|
|
||||||
.action-btn:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
background: #4a3216;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Deity Selector Overlay */
|
|
||||||
.deity-selector-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0; left: 0; width: 100%; height: 100%;
|
|
||||||
background: rgba(0,0,0,0.7);
|
|
||||||
z-index: 20;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deity-selector-menu {
|
|
||||||
background: #4a3216;
|
|
||||||
border: 2px solid #FFD700;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 6px;
|
|
||||||
width: 85%;
|
|
||||||
max-height: 75%;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-header {
|
|
||||||
text-align: center; color: #FFD700; font-family: 'DotGothic16', monospace; margin-bottom: 6px; font-weight: bold; font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deity-option {
|
|
||||||
display: flex; align-items: center; gap: 6px; padding: 4px; margin: 3px 0;
|
|
||||||
background: #654321; border: 1px solid transparent; border-radius: 3px; cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
z-index: 25;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
}
|
|
||||||
.deity-option:hover {
|
|
||||||
border-color: #FFD700;
|
|
||||||
background: #7a5238;
|
|
||||||
}
|
|
||||||
.deity-option:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
.deity-option.active { background: #8B4513; border-color: #FFD700; }
|
|
||||||
.deity-option span { font-family: 'DotGothic16', monospace; font-size: 9px; color: #FFD700; }
|
|
||||||
.deity-icon-small { width: 16px; height: 16px; background: #FFD700; border: 1px solid #FFA500; border-radius: 50%; }
|
|
||||||
|
|
||||||
/* Deity Icons - Pixel Art Placeholders */
|
|
||||||
.deity-mazu,
|
|
||||||
.deity-earthgod,
|
|
||||||
.deity-matchmaker,
|
|
||||||
.deity-wenchang,
|
|
||||||
.deity-guanyin {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deity-mazu::after { content: '媽'; }
|
|
||||||
.deity-earthgod::after { content: '土'; }
|
|
||||||
.deity-matchmaker::after { content: '月'; }
|
|
||||||
.deity-wenchang::after { content: '文'; }
|
|
||||||
.deity-guanyin::after { content: '觀'; }
|
|
||||||
|
|
||||||
/* Prayer Option Icons */
|
|
||||||
.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,
|
|
||||||
-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;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -1,617 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="game-overlay" @click.self="handleClose">
|
|
||||||
<div class="game-container">
|
|
||||||
<h2 class="game-title">猜拳遊戲</h2>
|
|
||||||
|
|
||||||
<div v-if="!gameStarted" class="game-intro">
|
|
||||||
<p>3回合制</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="gameStarted" class="game-status">
|
|
||||||
<div class="score">
|
|
||||||
<span>你: {{ playerScore }}</span>
|
|
||||||
<span>第{{ currentRound }}/3局</span>
|
|
||||||
<span>電腦: {{ cpuScore }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="result" class="result-display" ref="resultDisplayRef">
|
|
||||||
<div class="hands" :class="{ 'shake': waiting }">
|
|
||||||
<div class="hand player-hand" :class="{ 'slide-in-left': !waiting }">
|
|
||||||
<div class="hand-icon" :class="`icon-${playerChoice}`"></div>
|
|
||||||
</div>
|
|
||||||
<div class="vs">VS</div>
|
|
||||||
<div class="hand cpu-hand" :class="{ 'slide-in-right': !waiting }">
|
|
||||||
<div class="hand-icon" :class="`icon-${cpuChoice}`"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="result-text" :class="[result, { 'bounce-in': !waiting }]">{{ resultText }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!gameOver" class="choices">
|
|
||||||
<button
|
|
||||||
v-for="choice in choices"
|
|
||||||
:key="choice.id"
|
|
||||||
class="choice-btn"
|
|
||||||
@click="play(choice.id)"
|
|
||||||
:disabled="waiting"
|
|
||||||
>
|
|
||||||
<div class="choice-icon" :class="`icon-${choice.id}`"></div>
|
|
||||||
<span>{{ choice.name }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="gameOver" class="game-over">
|
|
||||||
<div class="final-result" :class="finalResult">
|
|
||||||
<div class="result-icon" :class="`icon-${finalResult}`"></div>
|
|
||||||
<h3>{{ finalResultText }}</h3>
|
|
||||||
</div>
|
|
||||||
<button class="action-btn" @click="playAgain">再玩一次</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="close-btn" @click="handleClose">
|
|
||||||
{{ gameOver ? '完成' : '取消' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed } from 'vue';
|
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'complete']);
|
|
||||||
|
|
||||||
const choices = [
|
|
||||||
{ id: 'rock', name: '石頭' },
|
|
||||||
{ id: 'paper', name: '布' },
|
|
||||||
{ id: 'scissors', name: '剪刀' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const gameStarted = ref(false);
|
|
||||||
const gameOver = ref(false);
|
|
||||||
const currentRound = ref(1);
|
|
||||||
const playerScore = ref(0);
|
|
||||||
const cpuScore = ref(0);
|
|
||||||
const playerChoice = ref('');
|
|
||||||
const cpuChoice = ref('');
|
|
||||||
const result = ref('');
|
|
||||||
const waiting = ref(false);
|
|
||||||
|
|
||||||
const resultText = computed(() => {
|
|
||||||
if (result.value === 'win') return '你贏了!';
|
|
||||||
if (result.value === 'lose') return '你輸了!';
|
|
||||||
return '平手!';
|
|
||||||
});
|
|
||||||
|
|
||||||
const finalResult = computed(() => {
|
|
||||||
if (playerScore.value > cpuScore.value) return 'win';
|
|
||||||
if (playerScore.value < cpuScore.value) return 'lose';
|
|
||||||
return 'draw';
|
|
||||||
});
|
|
||||||
|
|
||||||
const finalResultText = computed(() => {
|
|
||||||
if (finalResult.value === 'win') return '勝利!';
|
|
||||||
if (finalResult.value === 'lose') return '失敗...';
|
|
||||||
return '平手';
|
|
||||||
});
|
|
||||||
|
|
||||||
function play(choice) {
|
|
||||||
if (waiting.value || gameOver.value) return;
|
|
||||||
|
|
||||||
gameStarted.value = true;
|
|
||||||
waiting.value = true;
|
|
||||||
playerChoice.value = choice;
|
|
||||||
|
|
||||||
// CPU makes a random choice
|
|
||||||
const cpuIndex = Math.floor(Math.random() * 3);
|
|
||||||
cpuChoice.value = choices[cpuIndex].id;
|
|
||||||
|
|
||||||
// Determine winner
|
|
||||||
setTimeout(() => {
|
|
||||||
const outcome = determineWinner(choice, cpuChoice.value);
|
|
||||||
result.value = outcome;
|
|
||||||
|
|
||||||
if (outcome === 'win') {
|
|
||||||
playerScore.value++;
|
|
||||||
} else if (outcome === 'lose') {
|
|
||||||
cpuScore.value++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if game is over
|
|
||||||
setTimeout(() => {
|
|
||||||
if (currentRound.value >= 3) {
|
|
||||||
gameOver.value = true;
|
|
||||||
} else {
|
|
||||||
currentRound.value++;
|
|
||||||
result.value = '';
|
|
||||||
waiting.value = false;
|
|
||||||
}
|
|
||||||
}, 1500);
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function determineWinner(player, cpu) {
|
|
||||||
if (player === cpu) return 'draw';
|
|
||||||
if (
|
|
||||||
(player === 'rock' && cpu === 'scissors') ||
|
|
||||||
(player === 'paper' && cpu === 'rock') ||
|
|
||||||
(player === 'scissors' && cpu === 'paper')
|
|
||||||
) {
|
|
||||||
return 'win';
|
|
||||||
}
|
|
||||||
return 'lose';
|
|
||||||
}
|
|
||||||
|
|
||||||
function playAgain() {
|
|
||||||
gameStarted.value = false;
|
|
||||||
gameOver.value = false;
|
|
||||||
currentRound.value = 1;
|
|
||||||
playerScore.value = 0;
|
|
||||||
cpuScore.value = 0;
|
|
||||||
result.value = '';
|
|
||||||
waiting.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClose() {
|
|
||||||
if (gameOver.value && finalResult.value === 'win') {
|
|
||||||
emit('complete', true); // Won the game
|
|
||||||
} else {
|
|
||||||
emit('complete', false); // Didn't win
|
|
||||||
}
|
|
||||||
emit('close');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.game-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.8);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 150;
|
|
||||||
font-family: 'DotGothic16', sans-serif;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-container {
|
|
||||||
background: #f5f5dc;
|
|
||||||
border: 4px solid #8b4513;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px;
|
|
||||||
width: 90%;
|
|
||||||
max-width: 280px;
|
|
||||||
max-height: 90vh;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
|
||||||
animation: slide-up 0.3s ease-out;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-title {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 13px;
|
|
||||||
margin: 0 0 6px 0;
|
|
||||||
color: #8b4513;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-intro {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
color: #8b4513;
|
|
||||||
font-size: 10px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-status {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #8b4513;
|
|
||||||
padding: 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-display {
|
|
||||||
margin: 6px 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hands {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hands.shake {
|
|
||||||
animation: shake-hands 0.5s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shake-hands {
|
|
||||||
0%, 100% { transform: translateX(0); }
|
|
||||||
25% { transform: translateX(-5px) rotate(-5deg); }
|
|
||||||
75% { transform: translateX(5px) rotate(5deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.hand {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hand.slide-in-left {
|
|
||||||
animation: slide-in-left 0.5s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hand.slide-in-right {
|
|
||||||
animation: slide-in-right 0.5s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide-in-left {
|
|
||||||
from {
|
|
||||||
transform: translateX(-50px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide-in-right {
|
|
||||||
from {
|
|
||||||
transform: translateX(50px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hand span {
|
|
||||||
font-size: 10px;
|
|
||||||
color: #8b4513;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hand-icon {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vs {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #8b4513;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-text {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 5px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-text.bounce-in {
|
|
||||||
animation: bounce-in 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce-in {
|
|
||||||
0% {
|
|
||||||
transform: scale(0);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-text.win {
|
|
||||||
background: #90EE90;
|
|
||||||
color: #006400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-text.lose {
|
|
||||||
background: #FFB6C1;
|
|
||||||
color: #8b0000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-text.draw {
|
|
||||||
background: #FFE4B5;
|
|
||||||
color: #8b4513;
|
|
||||||
}
|
|
||||||
|
|
||||||
.choices {
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.choice-btn {
|
|
||||||
flex: 1;
|
|
||||||
background: #fff;
|
|
||||||
border: 3px solid #8b4513;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 6px 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2px;
|
|
||||||
font-family: 'DotGothic16', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.choice-btn:hover:not(:disabled) {
|
|
||||||
background: #fffacd;
|
|
||||||
transform: scale(1.05);
|
|
||||||
animation: pulse 0.6s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { transform: scale(1.05); }
|
|
||||||
50% { transform: scale(1.1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.choice-btn:active:not(:disabled) {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
.choice-btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.choice-btn span {
|
|
||||||
font-size: 9px;
|
|
||||||
color: #8b4513;
|
|
||||||
}
|
|
||||||
|
|
||||||
.choice-icon, .hand-icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pixel Art Icons */
|
|
||||||
.icon-rock::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%) scale(2);
|
|
||||||
box-shadow:
|
|
||||||
-2px -2px 0 #888, -1px -2px 0 #888, 0 -2px 0 #888, 1px -2px 0 #888,
|
|
||||||
-3px -1px 0 #888, -2px -1px 0 #aaa, -1px -1px 0 #aaa, 0 -1px 0 #aaa, 1px -1px 0 #aaa, 2px -1px 0 #888,
|
|
||||||
-3px 0 0 #888, -2px 0 0 #aaa, -1px 0 0 #666, 0 0 0 #666, 1px 0 0 #aaa, 2px 0 0 #888,
|
|
||||||
-2px 1px 0 #888, -1px 1px 0 #aaa, 0 1px 0 #aaa, 1px 1px 0 #888,
|
|
||||||
-1px 2px 0 #888, 0 2px 0 #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-paper::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%) scale(2);
|
|
||||||
box-shadow:
|
|
||||||
-2px -3px 0 #ffcc99, -1px -3px 0 #ffcc99, 0 -3px 0 #ffcc99, 1px -3px 0 #ffcc99, 2px -3px 0 #ffcc99,
|
|
||||||
-3px -2px 0 #ffcc99, -2px -2px 0 #ffcc99, -1px -2px 0 #ffcc99, 0 -2px 0 #ffcc99, 1px -2px 0 #ffcc99, 2px -2px 0 #ffcc99, 3px -2px 0 #ffcc99,
|
|
||||||
-3px -1px 0 #ffcc99, -2px -1px 0 #ffcc99, -1px -1px 0 #ffcc99, 0 -1px 0 #ffcc99, 1px -1px 0 #ffcc99, 2px -1px 0 #ffcc99, 3px -1px 0 #ffcc99,
|
|
||||||
-3px 0 0 #ffcc99, -2px 0 0 #ffcc99, -1px 0 0 #ffaa77, 0 0 0 #ffaa77, 1px 0 0 #ffaa77, 2px 0 0 #ffcc99, 3px 0 0 #ffcc99,
|
|
||||||
-3px 1px 0 #ffcc99, -2px 1px 0 #ffcc99, -1px 1px 0 #ffcc99, 0 1px 0 #ffcc99, 1px 1px 0 #ffcc99, 2px 1px 0 #ffcc99, 3px 1px 0 #ffcc99;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-scissors::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%) scale(2);
|
|
||||||
box-shadow:
|
|
||||||
-3px -2px 0 #888, -2px -2px 0 #888,
|
|
||||||
-3px -1px 0 #888, -2px -1px 0 #888,
|
|
||||||
-2px 0 0 #888, -1px 0 0 #888, 0 0 0 #888, 1px 0 0 #888, 2px 0 0 #888,
|
|
||||||
-1px 1px 0 #888, 0 1px 0 #888, 1px 1px 0 #888,
|
|
||||||
2px -2px 0 #888, 3px -2px 0 #888,
|
|
||||||
2px -1px 0 #888, 3px -1px 0 #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-over {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.final-result {
|
|
||||||
font-size: 13px;
|
|
||||||
margin: 6px 0;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 5px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-icon {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Trophy icon for win */
|
|
||||||
.icon-win::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%) scale(2);
|
|
||||||
box-shadow:
|
|
||||||
/* Cup top */
|
|
||||||
-3px -4px 0 #ffd700, -2px -4px 0 #ffd700, -1px -4px 0 #ffd700, 0 -4px 0 #ffd700, 1px -4px 0 #ffd700, 2px -4px 0 #ffd700, 3px -4px 0 #ffd700,
|
|
||||||
/* Cup body */
|
|
||||||
-2px -3px 0 #ffd700, -1px -3px 0 #ffed4e, 0 -3px 0 #ffed4e, 1px -3px 0 #ffed4e, 2px -3px 0 #ffd700,
|
|
||||||
-2px -2px 0 #ffd700, -1px -2px 0 #ffed4e, 0 -2px 0 #ffed4e, 1px -2px 0 #ffed4e, 2px -2px 0 #ffd700,
|
|
||||||
-2px -1px 0 #ffd700, -1px -1px 0 #ffed4e, 0 -1px 0 #ffed4e, 1px -1px 0 #ffed4e, 2px -1px 0 #ffd700,
|
|
||||||
-2px 0 0 #ffd700, -1px 0 0 #ffd700, 0 0 0 #ffd700, 1px 0 0 #ffd700, 2px 0 0 #ffd700,
|
|
||||||
/* Base */
|
|
||||||
-1px 1px 0 #b8860b, 0 1px 0 #b8860b, 1px 1px 0 #b8860b,
|
|
||||||
-2px 2px 0 #b8860b, -1px 2px 0 #b8860b, 0 2px 0 #b8860b, 1px 2px 0 #b8860b, 2px 2px 0 #b8860b;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Broken heart icon for lose */
|
|
||||||
.icon-lose::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%) scale(2);
|
|
||||||
box-shadow:
|
|
||||||
/* Left side of heart */
|
|
||||||
-3px -2px 0 #ff6b6b, -2px -2px 0 #ff6b6b,
|
|
||||||
-4px -1px 0 #ff6b6b, -3px -1px 0 #ff6b6b, -2px -1px 0 #ff6b6b, -1px -1px 0 #ff6b6b,
|
|
||||||
-4px 0 0 #ff6b6b, -3px 0 0 #ff6b6b, -2px 0 0 #ff6b6b, -1px 0 0 #ff6b6b,
|
|
||||||
/* Right side of heart */
|
|
||||||
1px -2px 0 #ff6b6b, 2px -2px 0 #ff6b6b,
|
|
||||||
0 -1px 0 #ff6b6b, 1px -1px 0 #ff6b6b, 2px -1px 0 #ff6b6b, 3px -1px 0 #ff6b6b,
|
|
||||||
0 0 0 #ff6b6b, 1px 0 0 #ff6b6b, 2px 0 0 #ff6b6b, 3px 0 0 #ff6b6b,
|
|
||||||
/* Bottom crack (broken) */
|
|
||||||
-3px 1px 0 #ff6b6b, -1px 1px 0 #ff6b6b, 1px 1px 0 #ff6b6b,
|
|
||||||
-2px 2px 0 #ff6b6b, 0 2px 0 #ff6b6b,
|
|
||||||
-1px 3px 0 #ff6b6b;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Handshake icon for draw */
|
|
||||||
.icon-draw::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%) scale(2);
|
|
||||||
box-shadow:
|
|
||||||
/* Left hand */
|
|
||||||
-4px -1px 0 #ffcc99, -3px -1px 0 #ffcc99, -2px -1px 0 #ffcc99,
|
|
||||||
-4px 0 0 #ffcc99, -3px 0 0 #ffcc99, -2px 0 0 #ffcc99, -1px 0 0 #ffcc99,
|
|
||||||
-4px 1px 0 #ffcc99, -3px 1px 0 #ffcc99, -2px 1px 0 #ffcc99,
|
|
||||||
/* Right hand */
|
|
||||||
1px -1px 0 #ffcc99, 2px -1px 0 #ffcc99, 3px -1px 0 #ffcc99,
|
|
||||||
0 0 0 #ffcc99, 1px 0 0 #ffcc99, 2px 0 0 #ffcc99, 3px 0 0 #ffcc99,
|
|
||||||
1px 1px 0 #ffcc99, 2px 1px 0 #ffcc99, 3px 1px 0 #ffcc99;
|
|
||||||
}
|
|
||||||
|
|
||||||
.final-result h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.final-result.win {
|
|
||||||
background: #90EE90;
|
|
||||||
color: #006400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.final-result.lose {
|
|
||||||
background: #FFB6C1;
|
|
||||||
color: #8b0000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.final-result.draw {
|
|
||||||
background: #FFE4B5;
|
|
||||||
color: #8b4513;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: 5px;
|
|
||||||
background: #4CAF50;
|
|
||||||
border: 3px solid #2e7d32;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: white;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: 'DotGothic16', sans-serif;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:hover {
|
|
||||||
background: #45a049;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: 5px;
|
|
||||||
background: #cd853f;
|
|
||||||
border: 3px solid #8b4513;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: white;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: 'DotGothic16', sans-serif;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn:hover {
|
|
||||||
background: #d2691e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide-up {
|
|
||||||
from {
|
|
||||||
transform: translateY(50px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,416 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="inventory-screen" @click.self="$emit('close')">
|
|
||||||
<div class="inventory-container" @click="$emit('close')">
|
|
||||||
<div class="screen-title">背包</div>
|
|
||||||
|
|
||||||
<div class="inventory-grid">
|
|
||||||
<div
|
|
||||||
v-for="(item, index) in items"
|
|
||||||
:key="index"
|
|
||||||
class="inventory-slot"
|
|
||||||
:class="{ 'selected': selectedIndex === index, 'empty': !item }"
|
|
||||||
:draggable="!!item"
|
|
||||||
@dragstart="handleDragStart(index, $event)"
|
|
||||||
@dragover.prevent
|
|
||||||
@drop="handleDrop(index)"
|
|
||||||
@click.stop="selectItem(index)"
|
|
||||||
@mouseenter="item ? handleMouseEnter(item, $event) : null"
|
|
||||||
@mouseleave="handleMouseLeave"
|
|
||||||
@dblclick="item ? useItem() : null"
|
|
||||||
>
|
|
||||||
<template v-if="item">
|
|
||||||
<div class="item-icon" :class="item.iconClass"></div>
|
|
||||||
<div class="item-count" v-if="item.count > 1">x{{ item.count }}</div>
|
|
||||||
<div class="equipped-badge" v-if="isEquipped(item)">E</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Floating Tooltip -->
|
|
||||||
<div
|
|
||||||
v-if="hoveredItem"
|
|
||||||
class="floating-tooltip"
|
|
||||||
:style="tooltipStyle"
|
|
||||||
@mouseenter="cancelHideTooltip"
|
|
||||||
@mouseleave="handleMouseLeave"
|
|
||||||
>
|
|
||||||
<div class="tooltip-name">{{ hoveredItem.name }}</div>
|
|
||||||
<div class="tooltip-desc">{{ hoveredItem.description }}</div>
|
|
||||||
<div class="tooltip-footer">
|
|
||||||
<div class="tooltip-hint">雙擊使用</div>
|
|
||||||
<button class="tooltip-delete-btn" @click.stop="handleDeleteItem(hoveredItem)">
|
|
||||||
<div class="trash-icon-small"></div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
inventory: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
equippedItems: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'use-item', 'update:inventory']);
|
|
||||||
|
|
||||||
const selectedIndex = ref(-1);
|
|
||||||
const draggedIndex = ref(null);
|
|
||||||
const isDragging = ref(false);
|
|
||||||
|
|
||||||
const items = computed(() => props.inventory);
|
|
||||||
const selectedItem = computed(() => {
|
|
||||||
if (selectedIndex.value >= 0 && selectedIndex.value < items.value.length) {
|
|
||||||
return items.value[selectedIndex.value];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
function selectItem(index) {
|
|
||||||
if (items.value[index]) {
|
|
||||||
selectedIndex.value = index;
|
|
||||||
} else {
|
|
||||||
selectedIndex.value = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drag and Drop Logic
|
|
||||||
function handleDragStart(index, event) {
|
|
||||||
if (!items.value[index]) return;
|
|
||||||
draggedIndex.value = index;
|
|
||||||
isDragging.value = true;
|
|
||||||
// Set drag image or effect if needed
|
|
||||||
event.dataTransfer.effectAllowed = 'move';
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDrop(targetIndex) {
|
|
||||||
isDragging.value = false;
|
|
||||||
if (draggedIndex.value === null) return;
|
|
||||||
|
|
||||||
const newInventory = [...items.value];
|
|
||||||
const draggedItem = newInventory[draggedIndex.value];
|
|
||||||
const targetItem = newInventory[targetIndex];
|
|
||||||
|
|
||||||
// Swap items
|
|
||||||
newInventory[draggedIndex.value] = targetItem;
|
|
||||||
newInventory[targetIndex] = draggedItem;
|
|
||||||
|
|
||||||
emit('update:inventory', newInventory);
|
|
||||||
draggedIndex.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteItem(item) {
|
|
||||||
const index = items.value.indexOf(item);
|
|
||||||
if (index !== -1) {
|
|
||||||
const newInventory = [...items.value];
|
|
||||||
newInventory[index] = null;
|
|
||||||
emit('update:inventory', newInventory);
|
|
||||||
|
|
||||||
// Clear hover if deleted
|
|
||||||
if (hoveredItem.value === item) {
|
|
||||||
hoveredItem.value = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function useItem() {
|
|
||||||
// Use selected item if available, otherwise use hovered item (for double click)
|
|
||||||
const itemToUse = selectedItem.value || hoveredItem.value;
|
|
||||||
if (itemToUse) {
|
|
||||||
emit('use-item', itemToUse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isEquipped(item) {
|
|
||||||
return props.equippedItems.includes(item.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tooltip Logic
|
|
||||||
const hoveredItem = ref(null);
|
|
||||||
const tooltipStyle = ref({ top: '0px', left: '0px' });
|
|
||||||
let hideTooltipTimeout = null;
|
|
||||||
|
|
||||||
function handleMouseEnter(item, event) {
|
|
||||||
cancelHideTooltip();
|
|
||||||
hoveredItem.value = item;
|
|
||||||
updateTooltipPosition(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMouseLeave() {
|
|
||||||
// Delay hiding to allow moving to tooltip
|
|
||||||
hideTooltipTimeout = setTimeout(() => {
|
|
||||||
hoveredItem.value = null;
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelHideTooltip() {
|
|
||||||
if (hideTooltipTimeout) {
|
|
||||||
clearTimeout(hideTooltipTimeout);
|
|
||||||
hideTooltipTimeout = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTooltipPosition(event) {
|
|
||||||
const rect = event.target.getBoundingClientRect();
|
|
||||||
const tooltipWidth = 150; // Estimated width
|
|
||||||
const tooltipHeight = 80; // Estimated height
|
|
||||||
|
|
||||||
let top = rect.top - 10;
|
|
||||||
let left = rect.left + rect.width / 2;
|
|
||||||
let transform = 'translate(-50%, -100%)';
|
|
||||||
|
|
||||||
// Check top boundary
|
|
||||||
if (top < tooltipHeight) {
|
|
||||||
// Show below if not enough space above
|
|
||||||
top = rect.bottom + 10;
|
|
||||||
transform = 'translate(-50%, 0)';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check left/right boundary
|
|
||||||
const viewportWidth = window.innerWidth;
|
|
||||||
if (left - tooltipWidth / 2 < 0) {
|
|
||||||
left = tooltipWidth / 2 + 10;
|
|
||||||
} else if (left + tooltipWidth / 2 > viewportWidth) {
|
|
||||||
left = viewportWidth - tooltipWidth / 2 - 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltipStyle.value = {
|
|
||||||
top: top + 'px',
|
|
||||||
left: left + 'px',
|
|
||||||
transform: transform
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=DotGothic16&display=swap');
|
|
||||||
|
|
||||||
.inventory-screen {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.8);
|
|
||||||
z-index: 100;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inventory-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: #f0d09c;
|
|
||||||
border: none;
|
|
||||||
padding: 12px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen-title {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #3d2f1f;
|
|
||||||
border-bottom: 2px dashed #8b4513;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inventory-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 4px;
|
|
||||||
background: #e0b070;
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 2px solid #c49454;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inventory-slot {
|
|
||||||
aspect-ratio: 1;
|
|
||||||
background: #f8e8c8;
|
|
||||||
border: 2px solid #c49454;
|
|
||||||
border-radius: 4px;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inventory-slot:hover {
|
|
||||||
background: #fff;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.inventory-slot.selected {
|
|
||||||
border-color: #d32f2f;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 0 0 2px #d32f2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inventory-slot.empty {
|
|
||||||
background: rgba(0,0,0,0.05);
|
|
||||||
border-color: rgba(0,0,0,0.1);
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inventory-slot.empty:hover {
|
|
||||||
background: rgba(0,0,0,0.05);
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-count {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 2px;
|
|
||||||
right: 2px;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #333;
|
|
||||||
background: rgba(255,255,255,0.8);
|
|
||||||
padding: 0 2px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.equipped-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
right: 2px;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #fff;
|
|
||||||
background: #4caf50;
|
|
||||||
padding: 0 3px;
|
|
||||||
border-radius: 2px;
|
|
||||||
font-weight: bold;
|
|
||||||
box-shadow: 0 1px 2px rgba(0,0,0,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Item Icons (CSS Shapes) */
|
|
||||||
.icon-cookie::before {
|
|
||||||
content: '';
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
background: #d4a373;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: block;
|
|
||||||
box-shadow: inset -2px -2px 0 #8b4513;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-water::before {
|
|
||||||
content: '';
|
|
||||||
width: 12px;
|
|
||||||
height: 16px;
|
|
||||||
background: #4fc3f7;
|
|
||||||
border-radius: 40% 40% 40% 40% / 60% 60% 40% 40%;
|
|
||||||
display: block;
|
|
||||||
box-shadow: inset -2px -2px 0 #0288d1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-amulet::before {
|
|
||||||
content: '';
|
|
||||||
width: 14px;
|
|
||||||
height: 18px;
|
|
||||||
background: #ffeb3b;
|
|
||||||
border: 1px solid #fbc02d;
|
|
||||||
display: block;
|
|
||||||
box-shadow: 0 2px 0 rgba(0,0,0,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sunglasses::before {
|
|
||||||
content: '';
|
|
||||||
width: 18px;
|
|
||||||
height: 8px;
|
|
||||||
background: #333;
|
|
||||||
border-radius: 2px;
|
|
||||||
display: block;
|
|
||||||
box-shadow:
|
|
||||||
inset 1px 1px 0 #555,
|
|
||||||
4px 0 0 #000,
|
|
||||||
-4px 0 0 #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Floating Tooltip */
|
|
||||||
.floating-tooltip {
|
|
||||||
position: fixed; /* Use fixed to position relative to viewport */
|
|
||||||
background: rgba(0, 0, 0, 0.9);
|
|
||||||
border: 1px solid #fff;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
/* pointer-events: none; <-- Removed to allow clicking button */
|
|
||||||
z-index: 200; /* Above everything */
|
|
||||||
min-width: 120px;
|
|
||||||
max-width: 200px;
|
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-name {
|
|
||||||
color: #ffcc00;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-desc {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 10px;
|
|
||||||
line-height: 1.3;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 4px;
|
|
||||||
padding-top: 4px;
|
|
||||||
border-top: 1px dashed #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-hint {
|
|
||||||
color: #aaa;
|
|
||||||
font-size: 9px;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-delete-btn {
|
|
||||||
background: #d32f2f;
|
|
||||||
border: 1px solid #b71c1c;
|
|
||||||
border-radius: 2px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-delete-btn:hover {
|
|
||||||
background: #f44336;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trash-icon-small {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
background: #fff;
|
|
||||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z'/%3E%3C/svg%3E") no-repeat center;
|
|
||||||
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z'/%3E%3C/svg%3E") no-repeat center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -30,26 +30,21 @@
|
||||||
<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-if="mode === 'fortune'">
|
<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>
|
||||||
|
|
||||||
<!-- 招魂模式 -->
|
|
||||||
<template v-else-if="mode === 'resurrect'">
|
|
||||||
<button class="pixel-btn retry-btn" @click="$emit('resurrect-confirm')">確認</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -66,6 +61,10 @@ const props = defineProps({
|
||||||
consecutiveCount: {
|
consecutiveCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
|
},
|
||||||
|
selectedIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -130,6 +129,10 @@ function handleRetryFortune() {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
startToss();
|
startToss();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
startToss
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -269,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;
|
||||||
|
|
@ -307,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,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;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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>
|
||||||
|
|
@ -1,773 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="pet-info-screen" @click="$emit('close')">
|
|
||||||
<div class="info-container">
|
|
||||||
<!-- Tabs -->
|
|
||||||
<div class="info-tabs" @click.stop>
|
|
||||||
<button
|
|
||||||
class="tab-btn"
|
|
||||||
:class="{ active: activeTab === 0 }"
|
|
||||||
@click="activeTab = 0"
|
|
||||||
>
|
|
||||||
狀態
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="tab-btn"
|
|
||||||
:class="{ active: activeTab === 1 }"
|
|
||||||
@click="activeTab = 1"
|
|
||||||
>
|
|
||||||
成就
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Close Button Removed -->
|
|
||||||
|
|
||||||
<!-- Stats View -->
|
|
||||||
<div v-if="activeTab === 0" class="stats-view">
|
|
||||||
<!-- Stats Bars at Top (Pixel Style) -->
|
|
||||||
<div class="stats-section">
|
|
||||||
<div class="stat-item">
|
|
||||||
<div class="stat-label-row">
|
|
||||||
<span class="stat-label">飢餓</span>
|
|
||||||
<span class="stat-value">{{ displayHunger }}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="pixel-bar">
|
|
||||||
<div
|
|
||||||
v-for="i in 10"
|
|
||||||
:key="'hunger-' + i"
|
|
||||||
class="pixel-block"
|
|
||||||
:class="{
|
|
||||||
'filled': i <= Math.floor(displayHunger / 10),
|
|
||||||
'color-orange': i <= Math.floor(displayHunger / 10)
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-item">
|
|
||||||
<div class="stat-label-row">
|
|
||||||
<span class="stat-label">快樂</span>
|
|
||||||
<span class="stat-value">{{ displayHappiness }}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="pixel-bar">
|
|
||||||
<div
|
|
||||||
v-for="i in 10"
|
|
||||||
:key="'happiness-' + i"
|
|
||||||
class="pixel-block"
|
|
||||||
:class="{
|
|
||||||
'filled': i <= Math.floor(displayHappiness / 10),
|
|
||||||
'color-yellow': i <= Math.floor(displayHappiness / 10)
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-item">
|
|
||||||
<div class="stat-label-row">
|
|
||||||
<span class="stat-label">健康</span>
|
|
||||||
<span class="stat-value">{{ displayHealth }}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="pixel-bar">
|
|
||||||
<div
|
|
||||||
v-for="i in 10"
|
|
||||||
:key="'health-' + i"
|
|
||||||
class="pixel-block pixel-heart"
|
|
||||||
:class="{
|
|
||||||
'filled': i <= Math.floor(displayHealth / 10),
|
|
||||||
'color-red': i <= Math.floor(displayHealth / 10)
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-content">
|
|
||||||
<div class="info-title">═ 寵物資料 ═</div>
|
|
||||||
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">名字</span>
|
|
||||||
<span class="value">{{ petName }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">階段</span>
|
|
||||||
<span class="value">{{ stageText }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">年齡</span>
|
|
||||||
<span class="value">{{ age }}歲</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">身高</span>
|
|
||||||
<span class="value">{{ height }}cm</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">體重</span>
|
|
||||||
<span class="value">{{ weight }}kg</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-divider"></div>
|
|
||||||
|
|
||||||
<!-- Destiny -->
|
|
||||||
<div class="info-item destiny-item" v-if="destiny">
|
|
||||||
<span class="label">命格</span>
|
|
||||||
<div class="destiny-content">
|
|
||||||
<span class="value destiny-name">{{ destiny.name }}</span>
|
|
||||||
<span class="destiny-desc">{{ destiny.description }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item" v-else>
|
|
||||||
<span class="label">命格</span>
|
|
||||||
<span class="value">???</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-divider"></div>
|
|
||||||
|
|
||||||
<!-- V2 Stats -->
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">力量 (STR)</span>
|
|
||||||
<span class="value">{{ str }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">智力 (INT)</span>
|
|
||||||
<span class="value">{{ int }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">敏捷 (DEX)</span>
|
|
||||||
<span class="value">{{ dex }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">世代</span>
|
|
||||||
<span class="value">第 {{ generation }} 代</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">神明好感</span>
|
|
||||||
<span class="value">{{ deityFavor }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">HP</span>
|
|
||||||
<span class="value">{{ baseStats.hp }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">攻擊</span>
|
|
||||||
<span class="value">{{ baseStats.attack }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">防禦</span>
|
|
||||||
<span class="value">{{ baseStats.defense }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">速度</span>
|
|
||||||
<span class="value">{{ baseStats.speed }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Achievements View -->
|
|
||||||
<div v-if="activeTab === 1" class="achievements-view">
|
|
||||||
<div class="info-title">═ 成就列表 ═</div>
|
|
||||||
<div class="achievements-list">
|
|
||||||
<div
|
|
||||||
v-for="ach in achievements"
|
|
||||||
:key="ach.id"
|
|
||||||
class="achievement-item"
|
|
||||||
:class="{ unlocked: ach.unlocked }"
|
|
||||||
>
|
|
||||||
<div class="ach-icon">
|
|
||||||
<div v-if="ach.unlocked" :class="'pixel-icon-' + ach.id"></div>
|
|
||||||
<div v-else class="pixel-icon-locked"></div>
|
|
||||||
</div>
|
|
||||||
<div class="ach-info">
|
|
||||||
<div class="ach-name">{{ ach.name }}</div>
|
|
||||||
<div class="ach-desc">{{ ach.desc }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
petName: String,
|
|
||||||
stage: String,
|
|
||||||
poopCount: Number,
|
|
||||||
baseStats: Object,
|
|
||||||
hunger: {
|
|
||||||
type: Number,
|
|
||||||
default: 100
|
|
||||||
},
|
|
||||||
happiness: {
|
|
||||||
type: Number,
|
|
||||||
default: 100
|
|
||||||
},
|
|
||||||
health: {
|
|
||||||
type: Number,
|
|
||||||
default: 100
|
|
||||||
},
|
|
||||||
// v2 Stats
|
|
||||||
str: { type: Number, default: 0 },
|
|
||||||
int: { type: Number, default: 0 },
|
|
||||||
dex: { type: Number, default: 0 },
|
|
||||||
generation: { type: Number, default: 1 },
|
|
||||||
deityFavor: { type: Number, default: 0 },
|
|
||||||
destiny: { type: Object, default: null },
|
|
||||||
achievements: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const activeTab = ref(0);
|
|
||||||
|
|
||||||
defineEmits(['close']);
|
|
||||||
|
|
||||||
// Display values (ceiling for bars)
|
|
||||||
const displayHunger = computed(() => Math.ceil(props.hunger));
|
|
||||||
const displayHappiness = computed(() => Math.ceil(props.happiness));
|
|
||||||
const displayHealth = computed(() => Math.ceil(props.health));
|
|
||||||
|
|
||||||
const stageText = computed(() => {
|
|
||||||
const stageMap = {
|
|
||||||
'egg': '蛋',
|
|
||||||
'baby': '幼年',
|
|
||||||
'child': '成長',
|
|
||||||
'adult': '成熟'
|
|
||||||
};
|
|
||||||
return stageMap[props.stage] || props.stage;
|
|
||||||
});
|
|
||||||
|
|
||||||
const age = computed(() => {
|
|
||||||
const ageMap = { 'egg': 0, 'baby': 1, 'child': 3, 'adult': 7 };
|
|
||||||
return ageMap[props.stage] || 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const height = computed(() => {
|
|
||||||
const heightMap = { 'egg': 5, 'baby': 15, 'child': 30, 'adult': 45 };
|
|
||||||
return heightMap[props.stage] || 30;
|
|
||||||
});
|
|
||||||
|
|
||||||
const weight = computed(() => {
|
|
||||||
const weightMap = { 'egg': 0.5, 'baby': 2, 'child': 5, 'adult': 8 };
|
|
||||||
return weightMap[props.stage] || 5;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.pet-info-screen {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: #f0d09c;
|
|
||||||
z-index: 50;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
image-rendering: pixelated;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-container {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: min-content;
|
|
||||||
position: relative; /* For absolute positioning of close button */
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
position: absolute;
|
|
||||||
top: 6px;
|
|
||||||
right: 6px;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
background: #d32f2f;
|
|
||||||
border: 2px solid #8b4513;
|
|
||||||
color: white;
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 10;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-shadow: 0 2px 0 #8b4513;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn:active {
|
|
||||||
transform: translateY(2px);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tabs */
|
|
||||||
.info-tabs {
|
|
||||||
display: flex;
|
|
||||||
background: #c49454;
|
|
||||||
padding: 8px 6px 0 6px; /* Increased top padding */
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn {
|
|
||||||
flex: 1;
|
|
||||||
border: 2px solid transparent; /* Reserve space */
|
|
||||||
border-bottom: none;
|
|
||||||
background: #a67c43;
|
|
||||||
color: #3d2f1f;
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 4px 0;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 4px 4px 0 0;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
top: 2px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: 32px;
|
|
||||||
outline: none; /* Remove blue focus ring */
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn.active {
|
|
||||||
background: #f0d09c; /* Match body bg */
|
|
||||||
font-weight: bold;
|
|
||||||
top: 0;
|
|
||||||
border-color: #8b6f47;
|
|
||||||
border-bottom: 2px solid #f0d09c; /* Blend with content */
|
|
||||||
z-index: 2;
|
|
||||||
height: 34px; /* Slightly taller to cover bottom gap */
|
|
||||||
margin-bottom: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-view, .achievements-view {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stats Section - Pixel Style */
|
|
||||||
.stats-section {
|
|
||||||
padding: 12px 14px;
|
|
||||||
background: #c49454;
|
|
||||||
border-bottom: 3px solid #8b6f47;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #3d2f1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pixel-bar {
|
|
||||||
display: flex;
|
|
||||||
gap: 3px;
|
|
||||||
padding: 0;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pixel-block {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
background: rgba(139, 111, 71, 0.3);
|
|
||||||
border: 1px solid rgba(139, 111, 71, 0.5);
|
|
||||||
border-radius: 2px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pixel-block.filled {
|
|
||||||
box-shadow:
|
|
||||||
inset 1px 1px 0 rgba(255, 255, 255, 0.4),
|
|
||||||
0 2px 2px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pixel-block.color-orange.filled {
|
|
||||||
background: linear-gradient(135deg, #ff9966 0%, #ff6633 100%);
|
|
||||||
border-color: #cc4422;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pixel-block.color-yellow.filled {
|
|
||||||
background: linear-gradient(135deg, #ffdd66 0%, #ffcc33 100%);
|
|
||||||
border-color: #ccaa22;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pixel-block.color-red.filled {
|
|
||||||
background: linear-gradient(135deg, #ff6666 0%, #ff3333 100%);
|
|
||||||
border-color: #cc2222;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pixel Heart Shape */
|
|
||||||
.pixel-heart {
|
|
||||||
position: relative;
|
|
||||||
background: transparent !important;
|
|
||||||
border: none !important;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty heart outline */
|
|
||||||
.pixel-heart::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
box-shadow:
|
|
||||||
/* Top bumps - outline only */
|
|
||||||
-3px -3px 0 0 #8b6f47,
|
|
||||||
-2px -3px 0 0 #8b6f47,
|
|
||||||
2px -3px 0 0 #8b6f47,
|
|
||||||
3px -3px 0 0 #8b6f47,
|
|
||||||
/* Upper sides */
|
|
||||||
-4px -2px 0 0 #8b6f47,
|
|
||||||
4px -2px 0 0 #8b6f47,
|
|
||||||
/* Middle sides */
|
|
||||||
-4px -1px 0 0 #8b6f47,
|
|
||||||
4px -1px 0 0 #8b6f47,
|
|
||||||
-4px 0px 0 0 #8b6f47,
|
|
||||||
4px 0px 0 0 #8b6f47,
|
|
||||||
/* Lower sides */
|
|
||||||
-3px 1px 0 0 #8b6f47,
|
|
||||||
3px 1px 0 0 #8b6f47,
|
|
||||||
-2px 2px 0 0 #8b6f47,
|
|
||||||
2px 2px 0 0 #8b6f47,
|
|
||||||
-1px 3px 0 0 #8b6f47,
|
|
||||||
1px 3px 0 0 #8b6f47,
|
|
||||||
/* Bottom point */
|
|
||||||
0px 4px 0 0 #8b6f47;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Filled heart */
|
|
||||||
.pixel-heart.filled::before {
|
|
||||||
box-shadow:
|
|
||||||
/* Top bumps */
|
|
||||||
-3px -3px 0 0 #cc2222,
|
|
||||||
-2px -3px 0 0 #cc2222,
|
|
||||||
2px -3px 0 0 #cc2222,
|
|
||||||
3px -3px 0 0 #cc2222,
|
|
||||||
/* Second row */
|
|
||||||
-4px -2px 0 0 #ff3333,
|
|
||||||
-3px -2px 0 0 #ff6666,
|
|
||||||
-2px -2px 0 0 #ff6666,
|
|
||||||
-1px -2px 0 0 #ff6666,
|
|
||||||
0px -2px 0 0 #ff6666,
|
|
||||||
1px -2px 0 0 #ff6666,
|
|
||||||
2px -2px 0 0 #ff6666,
|
|
||||||
3px -2px 0 0 #ff6666,
|
|
||||||
4px -2px 0 0 #ff3333,
|
|
||||||
/* Third row */
|
|
||||||
-4px -1px 0 0 #ff3333,
|
|
||||||
-3px -1px 0 0 #ff6666,
|
|
||||||
-2px -1px 0 0 #ff8888,
|
|
||||||
-1px -1px 0 0 #ff8888,
|
|
||||||
0px -1px 0 0 #ff8888,
|
|
||||||
1px -1px 0 0 #ff8888,
|
|
||||||
2px -1px 0 0 #ff8888,
|
|
||||||
3px -1px 0 0 #ff6666,
|
|
||||||
4px -1px 0 0 #ff3333,
|
|
||||||
/* Middle row */
|
|
||||||
-4px 0px 0 0 #ff3333,
|
|
||||||
-3px 0px 0 0 #ff6666,
|
|
||||||
-2px 0px 0 0 #ff6666,
|
|
||||||
-1px 0px 0 0 #ff6666,
|
|
||||||
0px 0px 0 0 #ff6666,
|
|
||||||
1px 0px 0 0 #ff6666,
|
|
||||||
2px 0px 0 0 #ff6666,
|
|
||||||
3px 0px 0 0 #ff6666,
|
|
||||||
4px 0px 0 0 #ff3333,
|
|
||||||
/* Lower rows */
|
|
||||||
-3px 1px 0 0 #ff3333,
|
|
||||||
-2px 1px 0 0 #ff3333,
|
|
||||||
-1px 1px 0 0 #ff3333,
|
|
||||||
0px 1px 0 0 #ff3333,
|
|
||||||
1px 1px 0 0 #ff3333,
|
|
||||||
2px 1px 0 0 #ff3333,
|
|
||||||
3px 1px 0 0 #ff3333,
|
|
||||||
-2px 2px 0 0 #ff3333,
|
|
||||||
-1px 2px 0 0 #ff3333,
|
|
||||||
0px 2px 0 0 #ff3333,
|
|
||||||
1px 2px 0 0 #ff3333,
|
|
||||||
2px 2px 0 0 #ff3333,
|
|
||||||
-1px 3px 0 0 #cc2222,
|
|
||||||
0px 3px 0 0 #cc2222,
|
|
||||||
1px 3px 0 0 #cc2222,
|
|
||||||
/* Bottom point */
|
|
||||||
0px 4px 0 0 #cc2222;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-content {
|
|
||||||
padding: 12px 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-title {
|
|
||||||
text-align: center;
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #3d2f1f;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
padding-bottom: 6px;
|
|
||||||
border-bottom: 2px solid #c49454;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-divider {
|
|
||||||
height: 2px;
|
|
||||||
background: #c49454;
|
|
||||||
margin: 6px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 6px 10px;
|
|
||||||
background: #e0b070;
|
|
||||||
border: 2px solid #c49454;
|
|
||||||
box-shadow:
|
|
||||||
inset 1px 1px 0 #f0d09c,
|
|
||||||
inset -1px -1px 0 #8b6f47;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #3d2f1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #3d2f1f;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.value {
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #3d2f1f;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.destiny-item {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.destiny-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
padding-left: 4px;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.destiny-name {
|
|
||||||
color: #d32f2f; /* Red for emphasis */
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.destiny-desc {
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #665544;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Achievements */
|
|
||||||
.achievements-view {
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.achievements-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.achievement-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 8px;
|
|
||||||
background: #dcb880;
|
|
||||||
border: 2px solid #c49454;
|
|
||||||
border-radius: 4px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.achievement-item.unlocked {
|
|
||||||
background: #fff8e0;
|
|
||||||
border-color: #ffd700;
|
|
||||||
opacity: 1;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ach-icon {
|
|
||||||
font-size: 20px;
|
|
||||||
width: 30px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ach-info {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ach-name {
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #3d2f1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ach-desc {
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #665544;
|
|
||||||
}
|
|
||||||
/* Pixel Icons for Achievements */
|
|
||||||
.ach-icon {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Common Pixel Art Base */
|
|
||||||
.pixel-art {
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
transform: scale(2); /* Scale up the 1px pixels */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Locked Icon (Padlock) */
|
|
||||||
.pixel-icon-locked {
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
transform: scale(2);
|
|
||||||
color: #8b6f47;
|
|
||||||
box-shadow:
|
|
||||||
/* Body */
|
|
||||||
-3px 0, -2px 0, -1px 0, 0px 0, 1px 0, 2px 0, 3px 0,
|
|
||||||
-3px 1px, -2px 1px, -1px 1px, 0px 1px, 1px 1px, 2px 1px, 3px 1px,
|
|
||||||
-3px 2px, -2px 2px, -1px 2px, 0px 2px, 1px 2px, 2px 2px, 3px 2px,
|
|
||||||
-3px 3px, -2px 3px, -1px 3px, 0px 3px, 1px 3px, 2px 3px, 3px 3px,
|
|
||||||
/* Shackle */
|
|
||||||
-2px -1px, 2px -1px,
|
|
||||||
-2px -2px, 2px -2px,
|
|
||||||
-1px -3px, 0px -3px, 1px -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Newbie (Chick) - Yellow */
|
|
||||||
.pixel-icon-newbie {
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
transform: scale(2);
|
|
||||||
box-shadow:
|
|
||||||
/* Top feathers */
|
|
||||||
0px -5px #ffe135,
|
|
||||||
/* Head */
|
|
||||||
-2px -4px #ffe135, -1px -4px #ffe135, 0px -4px #ffe135, 1px -4px #ffe135, 2px -4px #ffe135,
|
|
||||||
-3px -3px #ffe135, -2px -3px #ffe135, -1px -3px #ffe135, 0px -3px #ffe135, 1px -3px #ffe135, 2px -3px #ffe135, 3px -3px #ffe135,
|
|
||||||
-3px -2px #ffe135, -2px -2px #000, -1px -2px #ffe135, 0px -2px #ffe135, 1px -2px #ffe135, 2px -2px #000, 3px -2px #ffe135, /* Eyes */
|
|
||||||
-3px -1px #ffe135, -2px -1px #ff69b4, -1px -1px #ffe135, 0px -1px #ff8c00, 1px -1px #ffe135, 2px -1px #ff69b4, 3px -1px #ffe135, /* Cheeks & Beak */
|
|
||||||
/* Body */
|
|
||||||
-3px 0px #ffe135, -2px 0px #ffe135, -1px 0px #ffe135, 0px 0px #ffe135, 1px 0px #ffe135, 2px 0px #ffe135, 3px 0px #ffe135,
|
|
||||||
-2px 1px #ffe135, -1px 1px #ffe135, 0px 1px #ffe135, 1px 1px #ffe135, 2px 1px #ffe135,
|
|
||||||
/* Feet */
|
|
||||||
-2px 2px #ff8c00, 2px 2px #ff8c00;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Veteran (Mario Style) */
|
|
||||||
.pixel-icon-veteran {
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
transform: scale(2);
|
|
||||||
box-shadow:
|
|
||||||
/* Hat */
|
|
||||||
-2px -6px #f00, -1px -6px #f00, 0px -6px #f00, 1px -6px #f00, 2px -6px #f00,
|
|
||||||
-3px -5px #f00, -2px -5px #f00, -1px -5px #f00, 0px -5px #f00, 1px -5px #f00, 2px -5px #f00, 3px -5px #f00,
|
|
||||||
/* Face */
|
|
||||||
-2px -4px #fc9, -1px -4px #fc9, 0px -4px #fc9, 1px -4px #fc9, 2px -4px #000, 3px -4px #fc9,
|
|
||||||
-3px -3px #fc9, -2px -3px #fc9, -1px -3px #fc9, 0px -3px #fc9, 1px -3px #000, 2px -3px #fc9, 3px -3px #fc9,
|
|
||||||
/* Mustache */
|
|
||||||
-1px -2px #000, 0px -2px #000, 1px -2px #000, 2px -2px #000,
|
|
||||||
/* Body */
|
|
||||||
-2px -1px #f00, -1px -1px #00f, 0px -1px #f00, 1px -1px #00f, 2px -1px #f00,
|
|
||||||
-2px 0px #f00, -1px 0px #00f, 0px 0px #00f, 1px 0px #00f, 2px 0px #f00,
|
|
||||||
-2px 1px #00f, -1px 1px #00f, 0px 1px #00f, 1px 1px #00f, 2px 1px #00f,
|
|
||||||
/* Shoes */
|
|
||||||
-2px 2px #8b4513, -1px 2px #8b4513, 1px 2px #8b4513, 2px 2px #8b4513;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Healthy (Donald Style) */
|
|
||||||
.pixel-icon-healthy {
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
transform: scale(2);
|
|
||||||
box-shadow:
|
|
||||||
/* Hat */
|
|
||||||
-2px -6px #1e90ff, -1px -6px #1e90ff, 0px -6px #1e90ff, 1px -6px #1e90ff,
|
|
||||||
-3px -5px #000, -2px -5px #1e90ff, -1px -5px #1e90ff, 0px -5px #1e90ff, 1px -5px #1e90ff, 2px -5px #000,
|
|
||||||
/* Face */
|
|
||||||
-2px -4px #fff, -1px -4px #fff, 0px -4px #fff, 1px -4px #fff,
|
|
||||||
-3px -3px #fff, -2px -3px #00f, -1px -3px #fff, 0px -3px #fff, 1px -3px #00f, 2px -3px #fff, /* Eyes */
|
|
||||||
/* Beak */
|
|
||||||
-2px -2px #ffa500, -1px -2px #ffa500, 0px -2px #ffa500, 1px -2px #ffa500,
|
|
||||||
-1px -1px #ffa500, 0px -1px #ffa500,
|
|
||||||
/* Body (Sailor) */
|
|
||||||
-2px 0px #1e90ff, -1px 0px #1e90ff, 0px 0px #d00, 1px 0px #1e90ff, 2px 0px #1e90ff, /* Bowtie */
|
|
||||||
-2px 1px #fff, -1px 1px #fff, 0px 1px #fff, 1px 1px #fff, 2px 1px #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Happy (Heart) */
|
|
||||||
.pixel-icon-happy {
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
transform: scale(2);
|
|
||||||
color: #ff1493;
|
|
||||||
box-shadow:
|
|
||||||
-2px -2px, -1px -2px, 1px -2px, 2px -2px,
|
|
||||||
-3px -1px, -2px -1px, -1px -1px, 0px -1px, 1px -1px, 2px -1px, 3px -1px,
|
|
||||||
-3px 0px, -2px 0px, -1px 0px, 0px 0px, 1px 0px, 2px 0px, 3px 0px,
|
|
||||||
-2px 1px, -1px 1px, 0px 1px, 1px 1px, 2px 1px,
|
|
||||||
-1px 2px, 0px 2px, 1px 2px,
|
|
||||||
0px 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="play-menu-overlay" @click.self="$emit('close')">
|
|
||||||
<div class="play-menu-container">
|
|
||||||
<h2 class="menu-title">選擇遊戲</h2>
|
|
||||||
|
|
||||||
<div class="game-options">
|
|
||||||
<button
|
|
||||||
class="game-option"
|
|
||||||
@click="selectGame('training')"
|
|
||||||
>
|
|
||||||
<div class="option-icon icon-training"></div>
|
|
||||||
<div class="option-name">訓練</div>
|
|
||||||
<div class="option-desc">攻擊訓練</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="game-option"
|
|
||||||
@click="selectGame('guessing')"
|
|
||||||
>
|
|
||||||
<div class="option-icon icon-rps"></div>
|
|
||||||
<div class="option-name">猜拳</div>
|
|
||||||
<div class="option-desc">剪刀石頭布</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="game-option"
|
|
||||||
@click="selectGame('ball')"
|
|
||||||
>
|
|
||||||
<div class="option-icon icon-ball"></div>
|
|
||||||
<div class="option-name">接球</div>
|
|
||||||
<div class="option-desc">反應小遊戲</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="close-btn" @click="$emit('close')">取消</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
const emit = defineEmits(['close', 'select']);
|
|
||||||
|
|
||||||
function selectGame(gameType) {
|
|
||||||
emit('select', gameType);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.play-menu-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 100;
|
|
||||||
font-family: 'DotGothic16', sans-serif;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-menu-container {
|
|
||||||
background: #f5f5dc;
|
|
||||||
border: 4px solid #8b4513;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 15px;
|
|
||||||
min-width: 200px;
|
|
||||||
max-width: 90%;
|
|
||||||
max-height: 85%;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
|
||||||
overflow-y: auto;
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-title {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
color: #8b4513;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-options {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-option {
|
|
||||||
background: #fff;
|
|
||||||
border: 3px solid #8b4513;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
text-align: center;
|
|
||||||
font-family: 'DotGothic16', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-option:hover {
|
|
||||||
background: #fffacd;
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-option:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-icon {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
margin: 0 auto 5px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pixel Art Icons */
|
|
||||||
.icon-training::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%) scale(2);
|
|
||||||
box-shadow:
|
|
||||||
/* Explosion/Impact shape */
|
|
||||||
0 0 0 #ff0000,
|
|
||||||
-2px -2px 0 #ff4400, 2px -2px 0 #ff4400,
|
|
||||||
-3px 0 0 #ff6600, -2px 0 0 #ff4400, 0 0 0 #ff0000, 2px 0 0 #ff4400, 3px 0 0 #ff6600,
|
|
||||||
-2px 2px 0 #ff4400, 2px 2px 0 #ff4400,
|
|
||||||
/* Outer glow */
|
|
||||||
-4px -1px 0 #ffaa00, 4px -1px 0 #ffaa00,
|
|
||||||
-4px 1px 0 #ffaa00, 4px 1px 0 #ffaa00,
|
|
||||||
-1px -4px 0 #ffaa00, 1px -4px 0 #ffaa00,
|
|
||||||
-1px 4px 0 #ffaa00, 1px 4px 0 #ffaa00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-rps::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%) scale(2);
|
|
||||||
box-shadow:
|
|
||||||
/* Fist shape */
|
|
||||||
-1px -3px 0 #ffcc99, 0 -3px 0 #ffcc99, 1px -3px 0 #ffcc99,
|
|
||||||
-2px -2px 0 #ffcc99, -1px -2px 0 #ffcc99, 0 -2px 0 #ffcc99, 1px -2px 0 #ffcc99, 2px -2px 0 #ffcc99,
|
|
||||||
-2px -1px 0 #ffcc99, -1px -1px 0 #ffcc99, 0 -1px 0 #ffcc99, 1px -1px 0 #ffcc99, 2px -1px 0 #ffcc99,
|
|
||||||
-2px 0 0 #ffcc99, -1px 0 0 #ffcc99, 0 0 0 #ffaa77, 1px 0 0 #ffcc99, 2px 0 0 #ffcc99,
|
|
||||||
-2px 1px 0 #ffcc99, -1px 1px 0 #ffcc99, 0 1px 0 #ffcc99, 1px 1px 0 #ffcc99, 2px 1px 0 #ffcc99,
|
|
||||||
-1px 2px 0 #ffcc99, 0 2px 0 #ffcc99, 1px 2px 0 #ffcc99;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-ball::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
background: transparent;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%) scale(2);
|
|
||||||
box-shadow:
|
|
||||||
/* Ball shape */
|
|
||||||
-1px -3px 0 #000, 0 -3px 0 #000, 1px -3px 0 #000,
|
|
||||||
-2px -2px 0 #000, -1px -2px 0 #fff, 0 -2px 0 #fff, 1px -2px 0 #fff, 2px -2px 0 #000,
|
|
||||||
-3px -1px 0 #000, -2px -1px 0 #fff, -1px -1px 0 #fff, 0 -1px 0 #000, 1px -1px 0 #fff, 2px -1px 0 #fff, 3px -1px 0 #000,
|
|
||||||
-3px 0 0 #000, -2px 0 0 #fff, -1px 0 0 #000, 0 0 0 #000, 1px 0 0 #000, 2px 0 0 #fff, 3px 0 0 #000,
|
|
||||||
-3px 1px 0 #000, -2px 1px 0 #fff, -1px 1px 0 #fff, 0 1px 0 #000, 1px 1px 0 #fff, 2px 1px 0 #fff, 3px 1px 0 #000,
|
|
||||||
-2px 2px 0 #000, -1px 2px 0 #fff, 0 2px 0 #fff, 1px 2px 0 #fff, 2px 2px 0 #000,
|
|
||||||
-1px 3px 0 #000, 0 3px 0 #000, 1px 3px 0 #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-name {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #8b4513;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-desc {
|
|
||||||
font-size: 10px;
|
|
||||||
color: #a0522d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
background: #cd853f;
|
|
||||||
border: 3px solid #8b4513;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: white;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: 'DotGothic16', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn:hover {
|
|
||||||
background: #d2691e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
</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,7 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="stats-bar">
|
<div class="stats-bar">
|
||||||
<div class="stats-content">
|
|
||||||
<div class="stat-rows">
|
|
||||||
<div class="stat-row">
|
<div class="stat-row">
|
||||||
<div class="stat-icon pixel-heart"></div>
|
<div class="stat-icon pixel-heart"></div>
|
||||||
<div class="pixel-bar">
|
<div class="pixel-bar">
|
||||||
|
|
@ -50,17 +48,10 @@
|
||||||
<span class="stat-value" :class="{ 'warning': displayHealth < 30 }">{{ displayHealth }}</span>
|
<span class="stat-value" :class="{ 'warning': displayHealth < 30 }">{{ displayHealth }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Info Icon Button -->
|
|
||||||
<button class="info-icon-btn" @click="$emit('toggle-info')" title="寵物資訊">
|
|
||||||
<div class="pixel-i-icon"></div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
hunger: {
|
hunger: {
|
||||||
|
|
@ -74,27 +65,9 @@ const props = defineProps({
|
||||||
health: {
|
health: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 100
|
default: 100
|
||||||
},
|
|
||||||
petName: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
stage: {
|
|
||||||
type: String,
|
|
||||||
default: 'adult'
|
|
||||||
},
|
|
||||||
poopCount: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
baseStats: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({ hp: 0, attack: 0, defense: 0, speed: 0 })
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(['toggle-info']);
|
|
||||||
|
|
||||||
// 向上取整顯示,只有小數扣到0才會掉下一個整數
|
// 向上取整顯示,只有小數扣到0才會掉下一個整數
|
||||||
const displayHunger = computed(() => Math.ceil(props.hunger));
|
const displayHunger = computed(() => Math.ceil(props.hunger));
|
||||||
const displayHappiness = computed(() => Math.ceil(props.happiness));
|
const displayHappiness = computed(() => Math.ceil(props.happiness));
|
||||||
|
|
@ -105,23 +78,10 @@ const displayHealth = computed(() => Math.ceil(props.health));
|
||||||
.stats-bar {
|
.stats-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 4px 8px 3px 8px;
|
||||||
background: rgba(155, 188, 15, 0.08);
|
background: rgba(155, 188, 15, 0.08);
|
||||||
border-bottom: 2px solid rgba(0, 0, 0, 0.1);
|
border-bottom: 2px solid rgba(0, 0, 0, 0.1);
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 4px 8px 3px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-rows {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-row {
|
.stat-row {
|
||||||
|
|
@ -282,206 +242,4 @@ const displayHealth = computed(() => Math.ceil(props.health));
|
||||||
0%, 100% { opacity: 1; }
|
0%, 100% { opacity: 1; }
|
||||||
50% { opacity: 0.5; }
|
50% { opacity: 0.5; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Info Icon Button - Game Boy Pixel Style */
|
|
||||||
.info-icon-btn {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: #9bbc0f;
|
|
||||||
border: 2px solid #306230;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0;
|
|
||||||
box-shadow:
|
|
||||||
inset -2px -2px 0 #0f380f,
|
|
||||||
inset 2px 2px 0 #8bac0f;
|
|
||||||
image-rendering: pixelated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pixel Art "i" Icon */
|
|
||||||
.pixel-i-icon {
|
|
||||||
width: 2px;
|
|
||||||
height: 2px;
|
|
||||||
background: #0f380f;
|
|
||||||
position: relative;
|
|
||||||
box-shadow:
|
|
||||||
/* Top dot */
|
|
||||||
0px -7px 0 #0f380f,
|
|
||||||
/* Vertical bar */
|
|
||||||
0px -3px 0 #0f380f, 0px -1px 0 #0f380f,
|
|
||||||
0px 0px 0 #0f380f, 0px 1px 0 #0f380f,
|
|
||||||
0px 3px 0 #0f380f, 0px 5px 0 #0f380f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-icon-btn:hover {
|
|
||||||
background: #8bac0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-icon-btn:active {
|
|
||||||
box-shadow:
|
|
||||||
inset 2px 2px 0 #0f380f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Backdrop Overlay */
|
|
||||||
.info-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(15, 56, 15, 0.7);
|
|
||||||
z-index: 999;
|
|
||||||
animation: fadeIn 0.2s steps(4);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bottom Slide-out Info Panel - Game Boy Style */
|
|
||||||
.info-panel {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background: #9bbc0f;
|
|
||||||
border-top: 4px solid #0f380f;
|
|
||||||
z-index: 1000;
|
|
||||||
max-height: 50vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
animation: slideUp 0.2s steps(5);
|
|
||||||
box-shadow:
|
|
||||||
0 -2px 0 #306230,
|
|
||||||
0 -4px 8px rgba(0, 0, 0, 0.3);
|
|
||||||
image-rendering: pixelated;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideUp {
|
|
||||||
from {
|
|
||||||
transform: translateY(100%);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-panel-header {
|
|
||||||
padding: 10px 16px;
|
|
||||||
background: #306230;
|
|
||||||
border-bottom: 2px solid #0f380f;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-title {
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #9bbc0f;
|
|
||||||
text-shadow: 1px 1px 0 #0f380f;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-indicator {
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #8bac0f;
|
|
||||||
background: #0f380f;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border: 1px solid #306230;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-panel-body {
|
|
||||||
padding: 12px 16px 8px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
min-height: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-header {
|
|
||||||
text-align: center;
|
|
||||||
padding: 4px 0 8px;
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #306230;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: #8bac0f;
|
|
||||||
border: 2px solid #306230;
|
|
||||||
box-shadow:
|
|
||||||
inset 1px 1px 0 #9bbc0f,
|
|
||||||
inset -1px -1px 0 #0f380f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #0f380f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #0f380f;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-nav {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 16px;
|
|
||||||
background: #306230;
|
|
||||||
border-top: 2px solid #0f380f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn {
|
|
||||||
width: 36px;
|
|
||||||
height: 28px;
|
|
||||||
background: #8bac0f;
|
|
||||||
border: 2px solid #0f380f;
|
|
||||||
color: #0f380f;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: none;
|
|
||||||
box-shadow:
|
|
||||||
inset 1px 1px 0 #9bbc0f,
|
|
||||||
inset -1px -1px 0 #306230;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn:hover {
|
|
||||||
background: #9bbc0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn:active {
|
|
||||||
box-shadow: inset 2px 2px 0 #0f380f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-hint {
|
|
||||||
font-family: 'DotGothic16', monospace;
|
|
||||||
font-size: 9px;
|
|
||||||
color: #8bac0f;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,32 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="top-menu">
|
<div class="top-menu">
|
||||||
<button class="icon-btn icon-stats" @click="$emit('info')" title="Status"></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('playMenu')" :disabled="disabled" title="Play"></button>
|
:class="{ active: isMenuOpen && selectedIndex === 0 }"
|
||||||
<button class="icon-btn icon-temple" @click="$emit('temple')" :disabled="disabled" title="Temple"></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,10 +35,18 @@ const props = defineProps({
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
isMenuOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
selectedIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(['info', 'feed', 'playMenu', 'temple']);
|
defineEmits(['stats', 'feed', 'play', 'sleep']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -36,10 +67,20 @@ defineEmits(['info', 'feed', 'playMenu', 'temple']);
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,26 +137,23 @@ defineEmits(['info', 'feed', 'playMenu', 'temple']);
|
||||||
-2px 4px 0 #4444ff, 0px 4px 0 #4444ff, 2px 4px 0 #4444ff;
|
-2px 4px 0 #4444ff, 0px 4px 0 #4444ff, 2px 4px 0 #4444ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Temple Icon (廟宇) */
|
/* Sleep Icon (Moon) */
|
||||||
.icon-temple::before {
|
.icon-sleep::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: #D2691E;
|
background: #ffcc00;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
/* 屋頂 */
|
0px -6px 0 #ffcc00, 2px -6px 0 #ffcc00,
|
||||||
-4px -6px 0 #8B4513, -2px -6px 0 #8B4513, 0px -6px 0 #8B4513, 2px -6px 0 #8B4513, 4px -6px 0 #8B4513,
|
-2px -4px 0 #ffcc00, 0px -4px 0 #ffcc00, 2px -4px 0 #ffcc00, 4px -4px 0 #ffcc00,
|
||||||
/* 屋簷 */
|
-2px -2px 0 #ffcc00, 0px -2px 0 #ffcc00, 4px -2px 0 #ffcc00,
|
||||||
-6px -4px 0 #D2691E, -4px -4px 0 #D2691E, -2px -4px 0 #D2691E, 0px -4px 0 #D2691E, 2px -4px 0 #D2691E, 4px -4px 0 #D2691E, 6px -4px 0 #D2691E,
|
-2px 0px 0 #ffcc00, 0px 0px 0 #ffcc00, 4px 0px 0 #ffcc00,
|
||||||
/* 柱子與牆 */
|
-2px 2px 0 #ffcc00, 0px 2px 0 #ffcc00, 4px 2px 0 #ffcc00,
|
||||||
-4px -2px 0 #8B4513, 4px -2px 0 #8B4513,
|
-2px 4px 0 #ffcc00, 0px 4px 0 #ffcc00, 2px 4px 0 #ffcc00, 4px 4px 0 #ffcc00,
|
||||||
-4px 0px 0 #8B4513, -2px 0px 0 #FFD700, 0px 0px 0 #FFD700, 2px 0px 0 #FFD700, 4px 0px 0 #8B4513,
|
0px 6px 0 #ffcc00, 2px 6px 0 #ffcc00;
|
||||||
-4px 2px 0 #8B4513, 4px 2px 0 #8B4513,
|
|
||||||
/* 底座 */
|
|
||||||
-4px 4px 0 #654321, -2px 4px 0 #654321, 0px 4px 0 #654321, 2px 4px 0 #654321, 4px 4px 0 #654321;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,330 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="training-layer" @click="handleTap">
|
|
||||||
<canvas ref="gameCanvas" class="game-canvas"></canvas>
|
|
||||||
|
|
||||||
<!-- 移除所有 UI,變成純觀賞模式 -->
|
|
||||||
<div class="training-ui">
|
|
||||||
<!-- 隱藏 UI -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'complete', 'attack']);
|
|
||||||
|
|
||||||
const gameCanvas = ref(null);
|
|
||||||
const gameStarted = ref(false);
|
|
||||||
const gameOver = ref(false);
|
|
||||||
const score = ref(0);
|
|
||||||
const attackCount = ref(0);
|
|
||||||
const totalAttacks = ref(3); // 改為 3 次
|
|
||||||
|
|
||||||
// 遊戲數據
|
|
||||||
let projectiles = [];
|
|
||||||
// let targets = []; // 移除目標
|
|
||||||
let explosions = [];
|
|
||||||
let animationId = null;
|
|
||||||
let ctx = null;
|
|
||||||
let canvasWidth = 300;
|
|
||||||
let canvasHeight = 150;
|
|
||||||
|
|
||||||
// 寵物位置 (假設在右側)
|
|
||||||
const PET_X = 260;
|
|
||||||
const PET_Y = 100; // 嘴巴的高度
|
|
||||||
|
|
||||||
function handleTap() {
|
|
||||||
// 自動模式,禁用點擊
|
|
||||||
}
|
|
||||||
|
|
||||||
function initGame() {
|
|
||||||
if (!gameCanvas.value) return;
|
|
||||||
const canvas = gameCanvas.value;
|
|
||||||
// 設置畫布大小為父容器大小
|
|
||||||
const rect = canvas.parentElement.getBoundingClientRect();
|
|
||||||
canvas.width = rect.width;
|
|
||||||
canvas.height = rect.height;
|
|
||||||
canvasWidth = rect.width;
|
|
||||||
canvasHeight = rect.height;
|
|
||||||
|
|
||||||
ctx = canvas.getContext('2d');
|
|
||||||
loop(); // 開始渲染循環
|
|
||||||
startGame(); // 自動開始
|
|
||||||
}
|
|
||||||
|
|
||||||
function startGame() {
|
|
||||||
gameStarted.value = true;
|
|
||||||
gameOver.value = false;
|
|
||||||
score.value = 0;
|
|
||||||
attackCount.value = 0;
|
|
||||||
projectiles = [];
|
|
||||||
explosions = [];
|
|
||||||
|
|
||||||
// 自動攻擊序列
|
|
||||||
scheduleAttack(1000);
|
|
||||||
scheduleAttack(2500);
|
|
||||||
scheduleAttack(4000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleAttack(delay) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!gameOver.value) {
|
|
||||||
attack();
|
|
||||||
}
|
|
||||||
}, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除 spawnTargetLoop 和 spawnTarget 函數
|
|
||||||
|
|
||||||
function attack() {
|
|
||||||
emit('attack'); // 通知父組件播放寵物攻擊動畫
|
|
||||||
attackCount.value++;
|
|
||||||
|
|
||||||
// 隨機決定攻擊模式:雙發 (填滿螢幕) 或 強力單發
|
|
||||||
const isDouble = Math.random() > 0.4; // 60% 機率雙發
|
|
||||||
|
|
||||||
// 發射位置修正:寵物在右側 (約 X=240),火球要從前方出現 (約 X=180)
|
|
||||||
const spawnX = 180;
|
|
||||||
const centerY = 80; // 畫面中心高度
|
|
||||||
|
|
||||||
if (isDouble) {
|
|
||||||
// 雙發模式:上下兩顆,填滿垂直空間
|
|
||||||
// 上方火球
|
|
||||||
projectiles.push({
|
|
||||||
x: spawnX,
|
|
||||||
y: centerY - 50,
|
|
||||||
speed: -4, // 速度減慢 (原本 -10)
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
color: '#000000'
|
|
||||||
});
|
|
||||||
// 下方火球
|
|
||||||
projectiles.push({
|
|
||||||
x: spawnX,
|
|
||||||
y: centerY + 10,
|
|
||||||
speed: -4, // 速度減慢
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
color: '#000000'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 強力單發:一顆超大火球
|
|
||||||
projectiles.push({
|
|
||||||
x: spawnX,
|
|
||||||
y: centerY - 32, // 居中
|
|
||||||
speed: -5, // 速度減慢 (原本 -12)
|
|
||||||
width: 64,
|
|
||||||
height: 64,
|
|
||||||
color: '#000000'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 檢查是否完成訓練
|
|
||||||
if (attackCount.value >= totalAttacks.value) {
|
|
||||||
setTimeout(() => {
|
|
||||||
gameOver.value = true;
|
|
||||||
handleClose(); // 自動結束
|
|
||||||
}, 2000); // 等最後一發飛完
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loop() {
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
// 清空畫布 (透明)
|
|
||||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
|
||||||
|
|
||||||
if (gameStarted.value) {
|
|
||||||
update();
|
|
||||||
draw();
|
|
||||||
}
|
|
||||||
|
|
||||||
animationId = requestAnimationFrame(loop);
|
|
||||||
}
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
if (gameOver.value) return;
|
|
||||||
|
|
||||||
// 更新子彈
|
|
||||||
for (let i = projectiles.length - 1; i >= 0; i--) {
|
|
||||||
const p = projectiles[i];
|
|
||||||
p.x += p.speed;
|
|
||||||
|
|
||||||
// 子彈飛出螢幕左側
|
|
||||||
if (p.x < -50) {
|
|
||||||
projectiles.splice(i, 1);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除碰撞檢測
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除目標更新邏輯
|
|
||||||
|
|
||||||
// 更新爆炸 (如果有保留的話)
|
|
||||||
updateExplosions();
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
// 移除繪製目標
|
|
||||||
|
|
||||||
// 繪製火球
|
|
||||||
for (const p of projectiles) {
|
|
||||||
drawFireball(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 繪製爆炸
|
|
||||||
ctx.fillStyle = '#ff0000';
|
|
||||||
for (const e of explosions) {
|
|
||||||
ctx.fillRect(e.x, e.y, 4, 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 繪製像素塊 (模擬 V-Pet 的點陣)
|
|
||||||
function drawPixelBlock(x, y, w, h, color) {
|
|
||||||
const pixelSize = 4;
|
|
||||||
ctx.fillStyle = color || '#000000';
|
|
||||||
ctx.fillRect(x, y, w, h);
|
|
||||||
// 簡單的邊框
|
|
||||||
ctx.strokeStyle = 'rgba(0,0,0,0.3)';
|
|
||||||
ctx.strokeRect(x, y, w, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 繪製火球 (動態大小)
|
|
||||||
function drawFireball(p) {
|
|
||||||
const { x, y, width, height } = p;
|
|
||||||
// 根據寬度計算像素大小,保持 4x4 的網格比例
|
|
||||||
const pixelSize = width / 4;
|
|
||||||
|
|
||||||
// 經典 V-Pet 火球形狀 (4x4)
|
|
||||||
const pattern = [
|
|
||||||
[0,1,1,0],
|
|
||||||
[1,1,1,1],
|
|
||||||
[1,1,1,1],
|
|
||||||
[0,1,1,0]
|
|
||||||
];
|
|
||||||
|
|
||||||
ctx.fillStyle = p.color || '#000000';
|
|
||||||
pattern.forEach((row, r) => {
|
|
||||||
row.forEach((col, c) => {
|
|
||||||
if (col) {
|
|
||||||
ctx.fillRect(x + c*pixelSize, y + r*pixelSize, pixelSize, pixelSize);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createExplosion(x, y) {
|
|
||||||
for(let i=0; i<8; i++) {
|
|
||||||
explosions.push({
|
|
||||||
x: x + 10,
|
|
||||||
y: y + 10,
|
|
||||||
vx: (Math.random() - 0.5) * 4,
|
|
||||||
vy: (Math.random() - 0.5) * 4,
|
|
||||||
life: 10
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateExplosions() {
|
|
||||||
for(let i = explosions.length - 1; i >= 0; i--) {
|
|
||||||
const e = explosions[i];
|
|
||||||
e.x += e.vx;
|
|
||||||
e.y += e.vy;
|
|
||||||
e.life--;
|
|
||||||
if(e.life <= 0) explosions.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// drawPixelPet 移除,因為現在顯示真實寵物
|
|
||||||
|
|
||||||
function handleClose() {
|
|
||||||
if (gameOver.value) {
|
|
||||||
emit('complete', true);
|
|
||||||
} else {
|
|
||||||
emit('complete', false);
|
|
||||||
}
|
|
||||||
emit('close');
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initGame();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (animationId) cancelAnimationFrame(animationId);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.training-layer {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 50; /* 在寵物之上,但在 UI 之下 */
|
|
||||||
pointer-events: auto; /* 允許點擊 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-canvas {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.training-ui {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score {
|
|
||||||
font-family: 'DotGothic16', sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
color: #8b4513;
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-hint {
|
|
||||||
margin-top: 40px;
|
|
||||||
font-family: 'DotGothic16', sans-serif;
|
|
||||||
color: #8b4513;
|
|
||||||
animation: pulse 1s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-over-msg {
|
|
||||||
margin-top: 40px;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
padding: 10px;
|
|
||||||
border: 2px solid #8b4513;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-align: center;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-over-msg button {
|
|
||||||
margin-top: 5px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
background: #4CAF50;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { opacity: 0.6; }
|
|
||||||
50% { opacity: 1; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
export function useEventSystem(petSystem) {
|
|
||||||
const { stats, state, triggerState } = petSystem;
|
|
||||||
|
|
||||||
const currentEvent = ref(null);
|
|
||||||
const eventHistory = ref([]);
|
|
||||||
|
|
||||||
// --- Event Database ---
|
|
||||||
// Categories: 'good', 'bad', 'weird'
|
|
||||||
const EVENT_DATABASE = [
|
|
||||||
// --- Good Events ---
|
|
||||||
{
|
|
||||||
id: 'found_charm',
|
|
||||||
type: 'good',
|
|
||||||
text: '撿到小符',
|
|
||||||
effect: (s) => { s.happiness = Math.min(100, s.happiness + 5); },
|
|
||||||
condition: () => true,
|
|
||||||
icon: 'pixel-charm'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'self_clean',
|
|
||||||
type: 'good',
|
|
||||||
text: '自己掃地',
|
|
||||||
effect: (s) => { if (s.poopCount > 0) s.poopCount--; },
|
|
||||||
condition: (s) => s.poopCount > 0,
|
|
||||||
icon: 'pixel-broom'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dance',
|
|
||||||
type: 'good',
|
|
||||||
text: '開心地跳舞',
|
|
||||||
effect: (s) => { s.happiness = Math.min(100, s.happiness + 10); },
|
|
||||||
condition: (s) => s.happiness > 70,
|
|
||||||
icon: 'pixel-note'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gift_apple',
|
|
||||||
type: 'good',
|
|
||||||
text: '給你一顆果子',
|
|
||||||
effect: (s) => { /* Add to inventory logic later */ s.hunger = Math.min(100, s.hunger + 5); },
|
|
||||||
condition: () => Math.random() < 0.3,
|
|
||||||
icon: 'pixel-apple'
|
|
||||||
},
|
|
||||||
|
|
||||||
// --- Bad Events ---
|
|
||||||
{
|
|
||||||
id: 'night_noise',
|
|
||||||
type: 'bad',
|
|
||||||
text: '半夜亂叫',
|
|
||||||
effect: (s) => { s.happiness = Math.max(0, s.happiness - 5); },
|
|
||||||
condition: (s) => isNight(),
|
|
||||||
icon: 'pixel-angry'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'overeat',
|
|
||||||
type: 'bad',
|
|
||||||
text: '偷吃太多',
|
|
||||||
effect: (s) => { s.hunger = Math.min(100, s.hunger + 20); s.health = Math.max(0, s.health - 5); },
|
|
||||||
condition: (s) => s.hunger > 80,
|
|
||||||
icon: 'pixel-meat'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'stomach_ache',
|
|
||||||
type: 'bad',
|
|
||||||
text: '胃痛',
|
|
||||||
effect: (s) => { s.health = Math.max(0, s.health - 10); },
|
|
||||||
condition: (s) => s.health < 50,
|
|
||||||
icon: 'pixel-skull'
|
|
||||||
},
|
|
||||||
|
|
||||||
// --- Weird Events ---
|
|
||||||
{
|
|
||||||
id: 'stare',
|
|
||||||
type: 'weird',
|
|
||||||
text: '盯著螢幕外面看...',
|
|
||||||
effect: () => { },
|
|
||||||
condition: () => true,
|
|
||||||
icon: 'pixel-eye'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'corner_squat',
|
|
||||||
type: 'weird',
|
|
||||||
text: '走到角落蹲著',
|
|
||||||
effect: (s) => { s.happiness = Math.max(0, s.happiness - 5); },
|
|
||||||
condition: () => true,
|
|
||||||
icon: 'pixel-cloud'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'glitch',
|
|
||||||
type: 'weird',
|
|
||||||
text: '#@!$%^&*',
|
|
||||||
effect: (s) => { if (s.int > 5) s.int++; },
|
|
||||||
condition: () => Math.random() < 0.1,
|
|
||||||
icon: 'pixel-glitch'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function isNight() {
|
|
||||||
const hour = new Date().getHours();
|
|
||||||
return hour >= 22 || hour < 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkEventTriggers() {
|
|
||||||
if (state.value === 'sleep' || state.value === 'dead') return;
|
|
||||||
|
|
||||||
// 10% chance to trigger an event per check
|
|
||||||
// Destiny Effect: Luck (福運) - Good events more likely?
|
|
||||||
// Destiny Effect: Spiritual (靈視) - Night events more likely
|
|
||||||
|
|
||||||
if (Math.random() < 0.1) {
|
|
||||||
triggerRandomEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function triggerRandomEvent() {
|
|
||||||
// Filter valid events
|
|
||||||
const validEvents = EVENT_DATABASE.filter(e => e.condition(stats.value));
|
|
||||||
if (validEvents.length === 0) return;
|
|
||||||
|
|
||||||
const event = validEvents[Math.floor(Math.random() * validEvents.length)];
|
|
||||||
|
|
||||||
// Apply effect
|
|
||||||
event.effect(stats.value);
|
|
||||||
|
|
||||||
// Set current event for UI
|
|
||||||
currentEvent.value = event;
|
|
||||||
eventHistory.value.unshift({ ...event, time: new Date() });
|
|
||||||
|
|
||||||
// Auto-clear event after 3 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
currentEvent.value = null;
|
|
||||||
}, 4000);
|
|
||||||
|
|
||||||
console.log('Event Triggered:', event.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentEvent,
|
|
||||||
eventHistory,
|
|
||||||
checkEventTriggers,
|
|
||||||
triggerRandomEvent
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,103 +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
|
||||||
|
|
||||||
// --- Destiny Data ---
|
|
||||||
const DESTINIES = [
|
|
||||||
{ id: 'luck', name: '福運', description: '籤詩好籤率 +10%', rarity: 1 },
|
|
||||||
{ id: 'diligence', name: '勤奮', description: '訓練遊戲獎勵 +20%', rarity: 1 },
|
|
||||||
{ id: 'gluttony', name: '暴食', description: '飢餓下降速度 +30%', rarity: 1 },
|
|
||||||
{ id: 'playful', name: '愛玩', description: 'Happiness 更快下降/更快上升', rarity: 1 },
|
|
||||||
{ id: 'purification', name: '淨化', description: '生病機率 -20%', rarity: 1 },
|
|
||||||
{ id: 'thirdeye', name: '天眼', description: '擲筊出聖筊機率微升', rarity: 2 },
|
|
||||||
{ id: 'medium', name: '冥感', description: '死亡後招魂成功率提升', rarity: 2 }
|
|
||||||
];
|
|
||||||
|
|
||||||
// --- 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: 1, // days (start at day 1)
|
health: 100,
|
||||||
poopCount: 0, // Number of poops on screen
|
poopCount: 0,
|
||||||
// v2 Stats
|
ageMinutes: 0, // Track age in minutes
|
||||||
str: 0, // 力量 (Fireball Game)
|
|
||||||
int: 0, // 智力 (Guessing Game)
|
|
||||||
dex: 0, // 敏捷 (Catch Ball)
|
|
||||||
generation: 1, // 輪迴世代
|
|
||||||
deityFavor: 0, // 神明好感度
|
|
||||||
destiny: null, // 天生命格 (Object)
|
|
||||||
// Deity System
|
|
||||||
currentDeity: 'mazu',
|
|
||||||
deityFavors: {
|
|
||||||
mazu: 0,
|
|
||||||
earthgod: 0,
|
|
||||||
matchmaker: 0,
|
|
||||||
wenchang: 0,
|
|
||||||
guanyin: 0
|
|
||||||
},
|
|
||||||
dailyPrayerCount: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
const achievements = ref([
|
// Tracking for evolution
|
||||||
{ id: 'newbie', name: '新手飼主', desc: '養育超過 1 天', unlocked: false, icon: '🥚' },
|
hungerEvents: 0,
|
||||||
{ id: 'veteran', name: '資深飼主', desc: '養育超過 7 天', unlocked: false, icon: '🏆' },
|
sicknessEvents: 0
|
||||||
{ id: 'healthy', name: '健康寶寶', desc: '3歲且健康 > 90', unlocked: false, icon: '💪' },
|
});
|
||||||
{ id: 'happy', name: '快樂天使', desc: '3歲且快樂 > 90', unlocked: false, icon: '💖' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// --- Internal Timers ---
|
// --- Internal Timers ---
|
||||||
let gameLoopId = null;
|
let gameLoopId = null;
|
||||||
let tickCount = 0;
|
const TICK_RATE = 1000; // 1 second per tick for easier calculation
|
||||||
const TICK_RATE = 3000; // 3 seconds per tick
|
const SECONDS_PER_MINUTE = 60; // Game time scale (1 real sec = 1 game sec for now)
|
||||||
const TICKS_PER_DAY = 20; // For testing: 1 minute = 1 day (usually 28800 for 24h)
|
|
||||||
|
|
||||||
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 assignDestiny() {
|
|
||||||
// Simple weighted random or just random for now
|
|
||||||
// Rarity 2 has lower chance
|
|
||||||
const roll = Math.random();
|
|
||||||
let pool = DESTINIES;
|
|
||||||
|
|
||||||
// 20% chance for rare destiny
|
|
||||||
if (roll < 0.2) {
|
|
||||||
pool = DESTINIES.filter(d => d.rarity === 2);
|
|
||||||
} else {
|
|
||||||
pool = DESTINIES.filter(d => d.rarity === 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const picked = pool[Math.floor(Math.random() * pool.length)];
|
|
||||||
stats.value.destiny = picked;
|
|
||||||
console.log('Assigned Destiny:', picked);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
// Destiny Effect: Gluttony (暴食) might increase poop chance? Or just hunger decay.
|
stats.value.hunger = Math.min(CONFIG.needs.hunger.max, stats.value.hunger + CONFIG.needs.hunger.feedRecover);
|
||||||
if (Math.random() < 0.15) { // 從 0.3 降到 0.15
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
@ -108,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,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;
|
||||||
}
|
}
|
||||||
|
|
@ -136,163 +97,124 @@ 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
|
||||||
// Destiny Effect: Gluttony (暴食) - Hunger decreases faster (+30%)
|
// Add 1/60th of a minute (since tick is 1 sec)
|
||||||
let hungerDecay = 0.05;
|
stats.value.ageMinutes += (TICK_RATE / 1000) / 60;
|
||||||
if (stats.value.destiny?.id === 'gluttony') {
|
|
||||||
hungerDecay *= 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destiny Effect: Playful (愛玩) - Happiness decreases faster
|
// Check Evolution
|
||||||
let happinessDecay = 0.08;
|
checkEvolution();
|
||||||
if (stats.value.destiny?.id === 'playful') {
|
|
||||||
happinessDecay *= 1.2; // Faster decay
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destiny Effect: DEX (敏捷) - Hunger decreases slower
|
if (stage.value === 'egg') return; // Egg doesn't have needs decay
|
||||||
// DEX 10 = -10% decay, DEX 50 = -50% decay
|
|
||||||
if (stats.value.dex > 0) {
|
// 2. Needs Decay (Per Minute converted to Per Tick)
|
||||||
const reduction = Math.min(0.5, stats.value.dex * 0.01); // Max 50% reduction
|
const decayFactor = (TICK_RATE / 1000) / 60; // Fraction of a minute passed
|
||||||
hungerDecay *= (1 - reduction);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.value !== 'sleep') {
|
if (state.value !== 'sleep') {
|
||||||
stats.value.hunger = Math.max(0, stats.value.hunger - hungerDecay);
|
stats.value.hunger = Math.max(0, stats.value.hunger - CONFIG.needs.hunger.decayPerMinute * decayFactor);
|
||||||
stats.value.happiness = Math.max(0, stats.value.happiness - happinessDecay);
|
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 - (hungerDecay * 0.3));
|
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 - (happinessDecay * 0.3));
|
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') {
|
||||||
// Destiny Effect: Purification (淨化) - Sickness chance -20%
|
|
||||||
// Deity Buff: 媽祖 - Sickness chance -15%
|
|
||||||
let sickChance = 0.1;
|
|
||||||
if (stats.value.destiny?.id === 'purification') {
|
|
||||||
sickChance *= 0.8;
|
|
||||||
}
|
|
||||||
if (stats.value.currentDeity === 'mazu' && stats.value.deityFavors?.mazu > 0) {
|
|
||||||
sickChance *= 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stats.value.health < 30 && state.value !== 'sick') {
|
|
||||||
if (Math.random() < sickChance) {
|
|
||||||
state.value = 'sick';
|
state.value = 'sick';
|
||||||
}
|
stats.value.sicknessEvents++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health Recovery (健康值可以緩慢恢復)
|
// 4. Mood Update (Simplified)
|
||||||
// 如果沒有便便、飢餓值和快樂值都高,健康值會緩慢恢復
|
updateMood();
|
||||||
let healthRecovery = 0.05;
|
|
||||||
|
|
||||||
// Deity Buff: 觀音 - Health 回復 +20%
|
// 5. Random Events
|
||||||
if (stats.value.currentDeity === 'guanyin' && stats.value.deityFavors?.guanyin > 0) {
|
checkRandomEvents(decayFactor);
|
||||||
healthRecovery *= 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deity Buff: 月老 - Happiness 回復 +25%
|
|
||||||
if (stats.value.currentDeity === 'matchmaker' && stats.value.deityFavors?.matchmaker > 0) {
|
|
||||||
// Apply to happiness decay reduction (slower decay = faster recovery)
|
|
||||||
happinessDecay *= 0.75;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 + healthRecovery);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Death Check (移除死亡機制,依照之前的討論)
|
|
||||||
// if (stats.value.health === 0) {
|
|
||||||
// state.value = 'dead';
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Evolution / Growth
|
|
||||||
tickCount++;
|
|
||||||
if (tickCount >= TICKS_PER_DAY) {
|
|
||||||
stats.value.age++;
|
|
||||||
tickCount = 0;
|
|
||||||
checkEvolution();
|
|
||||||
}
|
|
||||||
|
|
||||||
checkAchievements();
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkAchievements() {
|
|
||||||
if (!achievements.value[0].unlocked && stats.value.age >= 1) {
|
|
||||||
unlockAchievement(0);
|
|
||||||
}
|
|
||||||
if (!achievements.value[1].unlocked && stats.value.age >= 7) {
|
|
||||||
unlockAchievement(1);
|
|
||||||
}
|
|
||||||
if (!achievements.value[2].unlocked && stats.value.age >= 3 && stats.value.health >= 90) {
|
|
||||||
unlockAchievement(2);
|
|
||||||
}
|
|
||||||
if (!achievements.value[3].unlocked && stats.value.age >= 3 && stats.value.happiness >= 90) {
|
|
||||||
unlockAchievement(3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function unlockAchievement(index) {
|
|
||||||
if (!achievements.value[index].unlocked) {
|
|
||||||
achievements.value[index].unlocked = true;
|
|
||||||
triggerState('happy', 2000); // Celebrate achievement
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function unlockAllAchievements() {
|
|
||||||
achievements.value.forEach(a => a.unlocked = true);
|
|
||||||
triggerState('happy', 2000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkEvolution() {
|
function checkEvolution() {
|
||||||
// Simple evolution logic
|
// Current stage max age check
|
||||||
if (stage.value === 'baby' && stats.value.age >= 3) {
|
if (currentStageConfig.value && currentStageConfig.value.maxAgeMinutes !== Infinity) {
|
||||||
stage.value = 'child';
|
if (stats.value.ageMinutes >= currentStageConfig.value.maxAgeMinutes) {
|
||||||
triggerState('happy', 2000); // Celebrate
|
// Find next stage
|
||||||
} else if (stage.value === 'child' && stats.value.age >= 7) {
|
// Simple linear progression for now, or use evolutionRules
|
||||||
stage.value = 'adult';
|
const currentIndex = CONFIG.lifecycle.stages.findIndex(s => s.id === stage.value);
|
||||||
triggerState('happy', 2000);
|
if (currentIndex < CONFIG.lifecycle.stages.length - 1) {
|
||||||
|
const nextStage = CONFIG.lifecycle.stages[currentIndex + 1];
|
||||||
|
evolveTo(nextStage.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function evolveTo(newStageId) {
|
||||||
|
console.log(`Evolving from ${stage.value} to ${newStageId}`);
|
||||||
|
stage.value = newStageId;
|
||||||
|
triggerState('idle', 1000); // Celebration?
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMood() {
|
||||||
|
// Determine mood based on CONFIG.mood.states
|
||||||
|
// Priority: Angry > Sad > Happy > Neutral
|
||||||
|
const m = CONFIG.mood.states;
|
||||||
|
|
||||||
|
// 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 ---
|
// --- Helpers ---
|
||||||
function triggerState(tempState, duration) {
|
function triggerState(tempState, duration) {
|
||||||
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);
|
||||||
|
|
@ -300,19 +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';
|
|
||||||
stats.value.hunger = 50;
|
|
||||||
stats.value.happiness = 50;
|
|
||||||
stats.value.health = 100;
|
|
||||||
stats.value.poopCount = 0;
|
|
||||||
|
|
||||||
// v2: Assign Destiny
|
// Reset stats for baby
|
||||||
assignDestiny();
|
stats.value.hunger = CONFIG.needs.hunger.startValue;
|
||||||
|
stats.value.happiness = CONFIG.needs.happiness.startValue;
|
||||||
isCleaning.value = false;
|
stats.value.energy = CONFIG.needs.energy.startValue;
|
||||||
|
stats.value.cleanliness = CONFIG.needs.cleanliness.startValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,59 +238,16 @@ 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,
|
|
||||||
age: 1,
|
|
||||||
poopCount: 0,
|
poopCount: 0,
|
||||||
// v2 Reset
|
ageMinutes: 0,
|
||||||
str: 0,
|
hungerEvents: 0,
|
||||||
int: 0,
|
sicknessEvents: 0
|
||||||
dex: 0,
|
|
||||||
generation: 1,
|
|
||||||
deityFavor: 0,
|
|
||||||
destiny: null
|
|
||||||
};
|
};
|
||||||
tickCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resurrect() {
|
|
||||||
if (state.value !== 'dead') return;
|
|
||||||
|
|
||||||
state.value = 'idle';
|
|
||||||
stats.value.health = 50; // Revive with half health
|
|
||||||
stats.value.happiness = 50;
|
|
||||||
stats.value.hunger = 50;
|
|
||||||
|
|
||||||
// Penalty or Ghost Buff?
|
|
||||||
// For now just a console log, maybe visual effect later
|
|
||||||
console.log('Pet Resurrected!');
|
|
||||||
}
|
|
||||||
|
|
||||||
function reincarnate() {
|
|
||||||
// Inherit logic
|
|
||||||
const prevDestiny = stats.value.destiny;
|
|
||||||
const prevFavor = stats.value.deityFavor;
|
|
||||||
const nextGen = (stats.value.generation || 1) + 1;
|
|
||||||
|
|
||||||
// Reset everything
|
|
||||||
reset();
|
|
||||||
|
|
||||||
// Apply Inheritance
|
|
||||||
stats.value.generation = nextGen;
|
|
||||||
|
|
||||||
// 20% Favor inheritance
|
|
||||||
stats.value.deityFavor = Math.floor(prevFavor * 0.2);
|
|
||||||
|
|
||||||
// Destiny Inheritance (Optional: Maybe keep if it was a rare one?)
|
|
||||||
// For now, let's say if you had a Rare (2) destiny, you keep it.
|
|
||||||
// Otherwise, you get a new one on hatch.
|
|
||||||
if (prevDestiny && prevDestiny.rarity === 2) {
|
|
||||||
stats.value.destiny = prevDestiny;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Pet Reincarnated to Gen', nextGen);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Lifecycle ---
|
// --- Lifecycle ---
|
||||||
|
|
@ -395,11 +269,6 @@ export function usePetSystem() {
|
||||||
clean,
|
clean,
|
||||||
sleep,
|
sleep,
|
||||||
hatchEgg,
|
hatchEgg,
|
||||||
reset,
|
reset
|
||||||
achievements,
|
|
||||||
unlockAllAchievements,
|
|
||||||
assignDestiny, // Export for debug if needed
|
|
||||||
resurrect,
|
|
||||||
reincarnate
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,78 +2,6 @@
|
||||||
// 定義各種寵物的像素藝術數據
|
// 定義各種寵物的像素藝術數據
|
||||||
|
|
||||||
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 },
|
|
||||||
},
|
|
||||||
tinyTigerCatB: {
|
tinyTigerCatB: {
|
||||||
id: 'tinyTigerCatB',
|
id: 'tinyTigerCatB',
|
||||||
meta: {
|
meta: {
|
||||||
|
|
@ -83,8 +11,6 @@ export const SPRITE_PRESETS = {
|
||||||
element: 'normal',
|
element: 'normal',
|
||||||
description: '一隻活潑、黏人的小虎斑貓,喜歡被餵食和玩耍。'
|
description: '一隻活潑、黏人的小虎斑貓,喜歡被餵食和玩耍。'
|
||||||
},
|
},
|
||||||
|
|
||||||
// === 1. 生命週期設定 ===
|
|
||||||
lifecycle: {
|
lifecycle: {
|
||||||
baseLifeMinutes: 7 * 24 * 60,
|
baseLifeMinutes: 7 * 24 * 60,
|
||||||
stages: [
|
stages: [
|
||||||
|
|
@ -136,8 +62,6 @@ export const SPRITE_PRESETS = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// === 2. 需求衰減設定 ===
|
|
||||||
needs: {
|
needs: {
|
||||||
hunger: {
|
hunger: {
|
||||||
max: 100,
|
max: 100,
|
||||||
|
|
@ -177,8 +101,6 @@ export const SPRITE_PRESETS = {
|
||||||
extraChanceIfStarving: 0.02
|
extraChanceIfStarving: 0.02
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// === 3. 數值設定 ===
|
|
||||||
stats: {
|
stats: {
|
||||||
base: {
|
base: {
|
||||||
hp: 30,
|
hp: 30,
|
||||||
|
|
@ -192,8 +114,6 @@ export const SPRITE_PRESETS = {
|
||||||
adult: { hp: 1.4, attack: 1.3, defense: 1.2, speed: 1.1 }
|
adult: { hp: 1.4, attack: 1.3, defense: 1.2, speed: 1.1 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// === 4. 外觀 / Sprite 設定 ===
|
|
||||||
appearance: {
|
appearance: {
|
||||||
pixelSize: 3,
|
pixelSize: 3,
|
||||||
sprites: {
|
sprites: {
|
||||||
|
|
@ -272,6 +192,26 @@ export const SPRITE_PRESETS = {
|
||||||
'0000000000000000',
|
'0000000000000000',
|
||||||
'0000000000000000',
|
'0000000000000000',
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
adult: {
|
||||||
|
idle: [
|
||||||
|
'0000000000000000',
|
||||||
|
'0011000000110000',
|
||||||
|
'0124444111442100',
|
||||||
|
'0123222323221000',
|
||||||
|
'0122322223221000',
|
||||||
|
'0122522222522100',
|
||||||
|
'0125052225052100',
|
||||||
|
'0112223322221100',
|
||||||
|
'0122220222221000',
|
||||||
|
'0011222222110000',
|
||||||
|
'0001222222121000',
|
||||||
|
'0001222222121000',
|
||||||
|
'0001100110110000',
|
||||||
|
'0000000000000000',
|
||||||
|
'0000000000000000',
|
||||||
|
'0000000000000000',
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
palettes: {
|
palettes: {
|
||||||
|
|
@ -321,8 +261,6 @@ export const SPRITE_PRESETS = {
|
||||||
idleEmoteIntervalSec: 15
|
idleEmoteIntervalSec: 15
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// === 5. 裝備設定 ===
|
|
||||||
equipment: {
|
equipment: {
|
||||||
slots: ['head', 'face', 'neck', 'back'],
|
slots: ['head', 'face', 'neck', 'back'],
|
||||||
items: [
|
items: [
|
||||||
|
|
@ -346,153 +284,41 @@ export const SPRITE_PRESETS = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// === 6. 個性系統 ===
|
|
||||||
personality: {
|
personality: {
|
||||||
traits: [
|
traits: ['clingy', 'energetic'],
|
||||||
{
|
modifiers: {
|
||||||
id: 'clingy',
|
hungerDecay: 1.0,
|
||||||
name: '黏人',
|
happinessDecay: 1.2,
|
||||||
description: '喜歡被互動,不理牠會生氣',
|
energyDecay: 0.9
|
||||||
effects: {
|
|
||||||
decayRateMultiplier: { happiness: 1.2 },
|
|
||||||
idleActionChance: { 'seek_owner': 0.3 }
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'aggressive',
|
|
||||||
name: '暴躁',
|
|
||||||
description: '吃東西時容易咬碗',
|
|
||||||
effects: {
|
|
||||||
decayRateMultiplier: { hunger: 1.1 },
|
|
||||||
interactionReaction: { 'poke': 'bite' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'lazy',
|
|
||||||
name: '懶惰',
|
|
||||||
description: '容易睡過頭',
|
|
||||||
effects: {
|
|
||||||
decayRateMultiplier: { energy: 0.8 },
|
|
||||||
sleepDurationMultiplier: 1.2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'curious',
|
|
||||||
name: '好奇',
|
|
||||||
description: 'Idle 時會到處張望',
|
|
||||||
effects: {
|
|
||||||
idleActionChance: { 'explore': 0.4 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'shy',
|
|
||||||
name: '膽小',
|
|
||||||
description: '怕生',
|
|
||||||
effects: {
|
|
||||||
stressThreshold: 0.8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
defaultTrait: 'clingy'
|
|
||||||
},
|
|
||||||
|
|
||||||
// === 7. 情緒濾鏡(Mood System)===
|
|
||||||
mood: {
|
mood: {
|
||||||
states: [
|
system: {
|
||||||
{
|
updateIntervalMinutes: 10,
|
||||||
id: 'happy',
|
factors: ['happiness', 'hunger', 'health']
|
||||||
name: '開心',
|
|
||||||
condition: 'needs.happiness > 80 && needs.hunger > 50',
|
|
||||||
spriteModifier: 'bounce'
|
|
||||||
},
|
},
|
||||||
{
|
states: {
|
||||||
id: 'sleepy',
|
happy: { minHappiness: 80, spriteFace: 'happy' },
|
||||||
name: '愛睏',
|
neutral: { minHappiness: 40, spriteFace: 'neutral' },
|
||||||
condition: 'needs.energy < 30',
|
sad: { maxHappiness: 40, spriteFace: 'sad' },
|
||||||
spriteModifier: 'yawn'
|
angry: { maxHunger: 20, spriteFace: 'angry' }
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'naughty',
|
|
||||||
name: '調皮',
|
|
||||||
condition: 'needs.happiness > 90 && needs.energy > 90',
|
|
||||||
spriteModifier: 'prank'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'angry',
|
|
||||||
name: '生氣',
|
|
||||||
condition: 'needs.happiness < 20',
|
|
||||||
spriteModifier: 'turn_away'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sad',
|
|
||||||
name: '難過',
|
|
||||||
condition: 'needs.happiness < 40 || needs.sickness > 0',
|
|
||||||
spriteModifier: 'cry'
|
|
||||||
}
|
}
|
||||||
],
|
|
||||||
updateIntervalMinutes: 60
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// === 8. 隨機事件 ===
|
|
||||||
randomEvents: [
|
randomEvents: [
|
||||||
{
|
{
|
||||||
id: 'gift_flower',
|
id: 'trip_over',
|
||||||
name: '叼來一朵花',
|
chancePerMinute: 0.005,
|
||||||
chance: 0.05,
|
condition: { maxCoordination: 20 },
|
||||||
effects: { happiness: +20, message: '送你一朵花!' }
|
effect: { happiness: -5, health: -1 },
|
||||||
|
message: 'Tripped and fell!'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'play_toy',
|
id: 'zoomies',
|
||||||
name: '自己找玩具玩',
|
chancePerMinute: 0.01,
|
||||||
chance: 0.1,
|
condition: { minEnergy: 90 },
|
||||||
effects: { happiness: +10, animation: 'play' }
|
effect: { energy: -20, happiness: +10 },
|
||||||
},
|
message: 'Suddenly started running around!'
|
||||||
{
|
|
||||||
id: 'sneeze',
|
|
||||||
name: '打噴嚏',
|
|
||||||
chance: 0.05,
|
|
||||||
effects: { sickness: +10, animation: 'sneeze' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'find_treasure',
|
|
||||||
name: '挖到寶物',
|
|
||||||
chance: 0.01,
|
|
||||||
effects: { happiness: +50, message: '挖到金幣了!' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'nightmare',
|
|
||||||
name: '做惡夢',
|
|
||||||
chance: 0.05,
|
|
||||||
condition: { time: 'night' },
|
|
||||||
effects: { happiness: -20, energy: -10, message: '做惡夢了...' }
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Backward Compatibility Layer
|
|
||||||
// Create flat versions for components that expect the old structure
|
|
||||||
export const SPRITE_PRESETS_FLAT = {
|
|
||||||
tigerChick: SPRITE_PRESETS.tigerChick,
|
|
||||||
tinyTigerCat: SPRITE_PRESETS.tinyTigerCat,
|
|
||||||
tinyTigerCatB: {
|
|
||||||
// Legacy flat structure for backward compatibility
|
|
||||||
name: SPRITE_PRESETS.tinyTigerCatB.meta.name,
|
|
||||||
pixelSize: SPRITE_PRESETS.tinyTigerCatB.appearance.pixelSize,
|
|
||||||
sprite: SPRITE_PRESETS.tinyTigerCatB.appearance.sprites.child.idle,
|
|
||||||
spriteMouthOpen: SPRITE_PRESETS.tinyTigerCatB.appearance.sprites.child.mouthOpen,
|
|
||||||
spriteEyesClosed: SPRITE_PRESETS.tinyTigerCatB.appearance.sprites.child.eyesClosed,
|
|
||||||
palette: SPRITE_PRESETS.tinyTigerCatB.appearance.palettes.default,
|
|
||||||
tailPixels: SPRITE_PRESETS.tinyTigerCatB.appearance.bodyParts.tailPixels,
|
|
||||||
earPixels: SPRITE_PRESETS.tinyTigerCatB.appearance.bodyParts.earPixels,
|
|
||||||
legFrontPixels: SPRITE_PRESETS.tinyTigerCatB.appearance.bodyParts.legFrontPixels,
|
|
||||||
legBackPixels: SPRITE_PRESETS.tinyTigerCatB.appearance.bodyParts.legBackPixels,
|
|
||||||
blushPixels: SPRITE_PRESETS.tinyTigerCatB.appearance.bodyParts.blushPixels,
|
|
||||||
eyePixels: SPRITE_PRESETS.tinyTigerCatB.appearance.bodyParts.eyePixels,
|
|
||||||
iconBackLeft: SPRITE_PRESETS.tinyTigerCatB.appearance.bodyParts.iconBackLeft,
|
|
||||||
iconBackRight: SPRITE_PRESETS.tinyTigerCatB.appearance.bodyParts.iconBackRight,
|
|
||||||
eggSprite: SPRITE_PRESETS.tinyTigerCatB.appearance.sprites.egg.idle,
|
|
||||||
eggPalette: SPRITE_PRESETS.tinyTigerCatB.appearance.palettes.egg
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue