Compare commits

..

No commits in common. "main" and "feat/tmp" have entirely different histories.

14 changed files with 128 additions and 3915 deletions

View File

@ -5,15 +5,12 @@ import DeviceScreen from './components/DeviceScreen.vue';
import PetGame from './components/PetGame.vue';
import Menu from './components/Menu.vue';
import { usePetSystem } from './composables/usePetSystem';
import { useEventSystem } from './composables/useEventSystem';
const currentScreen = ref('game');
const petGameRef = ref(null);
const showStats = ref(false); // Stats visibility
const debugAction = ref(null); // For passing debug commands to PetGame
// Initialize Pet System
const petSystem = usePetSystem();
const {
stage,
state,
@ -24,21 +21,8 @@ const {
clean,
isCleaning,
hatchEgg,
reset,
achievements,
unlockAllAchievements,
resurrect,
reincarnate
} = petSystem;
// Initialize Event System
const { currentEvent, checkEventTriggers, triggerRandomEvent } = useEventSystem(petSystem);
// Start Event Loop
setInterval(() => {
checkEventTriggers();
}, 10000); // Check every 10 seconds
reset
} = usePetSystem();
// Handle Action Menu Events
function handleAction(action) {
@ -54,29 +38,16 @@ function handleAction(action) {
clean();
break;
case 'play':
if (play()) {
if (petGameRef.value) {
petGameRef.value.startPlaying();
}
}
play();
break;
case 'sleep':
sleep();
break;
case 'medicine':
// Heal the pet with animation
// Heal the pet
if (state.value === 'sick') {
if (petGameRef.value) {
// Trigger medicine animation
petGameRef.value.startFeeding('medicine').then(() => {
stats.value.health = 100;
state.value = 'idle';
});
} else {
// Fallback if ref not ready
stats.value.health = 100;
state.value = 'idle';
}
}
break;
case 'stats':
@ -101,12 +72,6 @@ function handleAction(action) {
console.log('求籤功能');
// TODO:
break;
case 'resurrect':
resurrect();
break;
case 'reincarnate':
reincarnate();
break;
default:
console.log('Action not implemented:', action);
}
@ -116,10 +81,6 @@ function handleAction(action) {
function setPetState(newState) {
state.value = newState;
}
function triggerDebugAction(action, payload = null) {
debugAction.value = { type: action, payload, timestamp: Date.now() };
}
</script>
<template>
@ -134,9 +95,6 @@ function triggerDebugAction(action, payload = null) {
:stats="stats"
:isCleaning="isCleaning"
:showStats="showStats"
:debugAction="debugAction"
:achievements="achievements"
:currentEvent="currentEvent"
@update:state="state = $event"
@action="handleAction"
/>
@ -162,22 +120,6 @@ function triggerDebugAction(action, payload = null) {
<button v-if="stage === 'egg'" @click="hatchEgg()">🥚 Hatch Egg</button>
<button v-else @click="reset()">🔄 Reset to Egg</button>
</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>
</template>

View File

@ -2,7 +2,7 @@
<div class="action-menu">
<button class="icon-btn icon-clean" @click="$emit('clean')" :disabled="disabled || poopCount === 0" title="清理"></button>
<button class="icon-btn icon-medicine" @click="$emit('medicine')" :disabled="disabled || !isSick" title="治療"></button>
<button class="icon-btn icon-sleep" @click="$emit('sleep')" :disabled="disabled" title="關燈"></button>
<button class="icon-btn icon-training" @click="$emit('training')" :disabled="disabled" title="祈禱"></button>
<button class="icon-btn icon-backpack" @click="$emit('inventory')" :disabled="disabled" title="背包"></button>
</div>
</template>
@ -27,7 +27,7 @@ const props = defineProps({
}
});
defineEmits(['clean', 'medicine', 'sleep', 'inventory']);
defineEmits(['clean', 'medicine', 'training', 'inventory']);
</script>
<style scoped>
@ -90,28 +90,30 @@ defineEmits(['clean', 'medicine', 'sleep', 'inventory']);
0px 4px 0 #ff4444, 0px 6px 0 #ff4444;
}
/* Sleep Icon (Light Bulb/燈泡) - Enhanced */
.icon-sleep::before {
/* Training Icon (Praying Hands) */
.icon-training::before {
content: '';
position: absolute;
width: 2px;
height: 2px;
background: transparent;
background: #d4a574; /* 手的膚色 */
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
box-shadow:
/* 燈泡主體 (圓形) */
-2px -6px 0 #ffd700, 0px -6px 0 #ffd700, 2px -6px 0 #ffd700,
-4px -4px 0 #ffd700, -2px -4px 0 #ffd700, 0px -4px 0 #ffd700, 2px -4px 0 #ffd700, 4px -4px 0 #ffd700,
-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,
/* 燈泡底部 (螺旋) */
-1px 4px 0 #8B4513, 0px 4px 0 #8B4513, 1px 4px 0 #8B4513,
/* 光線 (向下) */
0px 6px 0 #ffd700, 0px 8px 0 #ffd700;
/* 光芒 - 頂部 */
0px -8px 0 #ffcc00,
-2px -6px 0 #ffcc00, 2px -6px 0 #ffcc00,
/* 合掌的手 - 簡化版 */
-2px -4px 0 #d4a574, 0px -4px 0 #d4a574, 2px -4px 0 #d4a574,
-2px -2px 0 #d4a574, 0px -2px 0 #d4a574, 2px -2px 0 #d4a574,
-2px 0px 0 #d4a574, 0px 0px 0 #d4a574, 2px 0px 0 #d4a574,
0px 2px 0 #d4a574, 0px 4px 0 #d4a574,
/* 光芒 - 左右 */
-6px -2px 0 #ffcc00, 6px -2px 0 #ffcc00,
-6px 0px 0 #ffcc00, 6px 0px 0 #ffcc00;
}
/* Backpack Icon */
@ -138,6 +140,4 @@ defineEmits(['clean', 'medicine', 'sleep', 'inventory']);
/* Bottom */
-2px 6px 0 #8d6e63, 0px 6px 0 #8d6e63, 2px 6px 0 #8d6e63;
}
</style>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -21,7 +21,6 @@
<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>
@ -55,11 +54,11 @@ import { ref, computed } from 'vue';
const props = defineProps({
inventory: {
type: Array,
default: () => []
},
equippedItems: {
type: Array,
default: () => []
default: () => [
{ id: 'cookie', name: '幸運餅乾', description: '增加一點快樂值', count: 5, iconClass: 'icon-cookie' },
{ id: 'water', name: '神水', description: '恢復健康', count: 2, iconClass: 'icon-water' },
{ id: 'amulet', name: '平安符', description: '保佑寵物平安', count: 1, iconClass: 'icon-amulet' }
]
}
});
@ -132,10 +131,6 @@ function useItem() {
}
}
function isEquipped(item) {
return props.equippedItems.includes(item.id);
}
// Tooltip Logic
const hoveredItem = ref(null);
const tooltipStyle = ref({ top: '0px', left: '0px' });
@ -287,19 +282,6 @@ function updateTooltipPosition(event) {
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) */
@ -333,19 +315,6 @@ function updateTooltipPosition(event) {
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 */

View File

@ -35,7 +35,7 @@
</template>
<!-- 求籤模式 -->
<template v-else-if="mode === 'fortune'">
<template v-else>
<!-- 失敗 (非聖筊) -->
<button v-if="resultType !== 'saint'" class="pixel-btn close-btn" @click="handleRetryFortune">重新求籤</button>
@ -45,11 +45,6 @@
<button v-else class="pixel-btn retry-btn" @click="$emit('finish-fortune')">查看籤詩</button>
</template>
</template>
<!-- 招魂模式 -->
<template v-else-if="mode === 'resurrect'">
<button class="pixel-btn retry-btn" @click="$emit('resurrect-confirm')">確認</button>
</template>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +1,6 @@
<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">
@ -106,43 +84,6 @@
<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>
@ -161,34 +102,11 @@
</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';
import { computed } from 'vue';
const props = defineProps({
petName: String,
@ -206,22 +124,9 @@ const props = defineProps({
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)
@ -275,76 +180,6 @@ const weight = computed(() => {
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 */
@ -569,205 +404,4 @@ const weight = computed(() => {
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>

View File

@ -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>

View File

@ -2,8 +2,8 @@
<div class="top-menu">
<button class="icon-btn icon-stats" @click="$emit('info')" title="Status"></button>
<button class="icon-btn icon-feed" @click="$emit('feed')" :disabled="disabled" title="Feed"></button>
<button class="icon-btn icon-play" @click="$emit('playMenu')" :disabled="disabled" title="Play"></button>
<button class="icon-btn icon-temple" @click="$emit('temple')" :disabled="disabled" title="Temple"></button>
<button class="icon-btn icon-play" @click="$emit('play')" :disabled="disabled" title="Play"></button>
<button class="icon-btn icon-sleep" @click="$emit('sleep')" :disabled="disabled" title="Sleep"></button>
</div>
</template>
@ -15,7 +15,7 @@ const props = defineProps({
}
});
defineEmits(['info', 'feed', 'playMenu', 'temple']);
defineEmits(['info', 'feed', 'play', 'sleep']);
</script>
<style scoped>
@ -96,26 +96,23 @@ defineEmits(['info', 'feed', 'playMenu', 'temple']);
-2px 4px 0 #4444ff, 0px 4px 0 #4444ff, 2px 4px 0 #4444ff;
}
/* Temple Icon (廟宇) */
.icon-temple::before {
/* Sleep Icon (Moon) */
.icon-sleep::before {
content: '';
position: absolute;
width: 2px;
height: 2px;
background: #D2691E;
background: #ffcc00;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-shadow:
/* 屋頂 */
-4px -6px 0 #8B4513, -2px -6px 0 #8B4513, 0px -6px 0 #8B4513, 2px -6px 0 #8B4513, 4px -6px 0 #8B4513,
/* 屋簷 */
-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,
/* 柱子與牆 */
-4px -2px 0 #8B4513, 4px -2px 0 #8B4513,
-4px 0px 0 #8B4513, -2px 0px 0 #FFD700, 0px 0px 0 #FFD700, 2px 0px 0 #FFD700, 4px 0px 0 #8B4513,
-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;
0px -6px 0 #ffcc00, 2px -6px 0 #ffcc00,
-2px -4px 0 #ffcc00, 0px -4px 0 #ffcc00, 2px -4px 0 #ffcc00, 4px -4px 0 #ffcc00,
-2px -2px 0 #ffcc00, 0px -2px 0 #ffcc00, 4px -2px 0 #ffcc00,
-2px 0px 0 #ffcc00, 0px 0px 0 #ffcc00, 4px 0px 0 #ffcc00,
-2px 2px 0 #ffcc00, 0px 2px 0 #ffcc00, 4px 2px 0 #ffcc00,
-2px 4px 0 #ffcc00, 0px 4px 0 #ffcc00, 2px 4px 0 #ffcc00, 4px 4px 0 #ffcc00,
0px 6px 0 #ffcc00, 2px 6px 0 #ffcc00;
}
</style>

View File

@ -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>

View File

@ -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
};
}

View File

@ -5,79 +5,24 @@ export function usePetSystem() {
const stage = ref('egg'); // egg, baby, adult
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 ---
const stats = ref({
hunger: 100, // 0-100 (0 = Starving)
happiness: 100, // 0-100 (0 = Depressed)
health: 100, // 0-100 (0 = Sick risk)
weight: 500, // grams
age: 1, // days (start at day 1)
poopCount: 0, // Number of poops on screen
// v2 Stats
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
age: 0, // days
poopCount: 0 // Number of poops on screen
});
const achievements = ref([
{ id: 'newbie', name: '新手飼主', desc: '養育超過 1 天', unlocked: false, icon: '🥚' },
{ id: 'veteran', name: '資深飼主', desc: '養育超過 7 天', unlocked: false, icon: '🏆' },
{ id: 'healthy', name: '健康寶寶', desc: '3歲且健康 > 90', unlocked: false, icon: '💪' },
{ id: 'happy', name: '快樂天使', desc: '3歲且快樂 > 90', unlocked: false, icon: '💖' }
]);
// --- Internal Timers ---
let gameLoopId = null;
let tickCount = 0;
const TICK_RATE = 3000; // 3 seconds per tick
const TICKS_PER_DAY = 20; // For testing: 1 minute = 1 day (usually 28800 for 24h)
const isCleaning = ref(false);
// --- 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() {
if (state.value === 'sleep' || state.value === 'dead' || stage.value === 'egg' || isCleaning.value) return false;
@ -93,7 +38,6 @@ export function usePetSystem() {
stats.value.weight += 50;
// Chance to poop after eating (降低機率)
// Destiny Effect: Gluttony (暴食) might increase poop chance? Or just hunger decay.
if (Math.random() < 0.15) { // 從 0.3 降到 0.15
setTimeout(() => {
if (stats.value.poopCount < 4) {
@ -145,32 +89,18 @@ export function usePetSystem() {
if (state.value === 'dead' || stage.value === 'egg') return;
// Decrease stats naturally
// Destiny Effect: Gluttony (暴食) - Hunger decreases faster (+30%)
let hungerDecay = 0.05;
if (stats.value.destiny?.id === 'gluttony') {
hungerDecay *= 1.3;
}
// Destiny Effect: Playful (愛玩) - Happiness decreases faster
let happinessDecay = 0.08;
if (stats.value.destiny?.id === 'playful') {
happinessDecay *= 1.2; // Faster decay
}
// Destiny Effect: DEX (敏捷) - Hunger decreases slower
// DEX 10 = -10% decay, DEX 50 = -50% decay
if (stats.value.dex > 0) {
const reduction = Math.min(0.5, stats.value.dex * 0.01); // Max 50% reduction
hungerDecay *= (1 - reduction);
}
// 目標:飢餓值約 30-60 分鐘下降 10%,快樂值約 20-40 分鐘下降 10%
// TICK_RATE = 3000ms (3秒), 600 ticks = 30分鐘
// 飢餓值每 tick -0.05 → 600 ticks = -30 (30分鐘下降30%)
// 快樂值每 tick -0.08 → 600 ticks = -48 (30分鐘下降48%)
if (state.value !== 'sleep') {
stats.value.hunger = Math.max(0, stats.value.hunger - hungerDecay);
stats.value.happiness = Math.max(0, stats.value.happiness - happinessDecay);
stats.value.hunger = Math.max(0, stats.value.hunger - 0.05);
stats.value.happiness = Math.max(0, stats.value.happiness - 0.08);
} else {
// Slower decay when sleeping (約 1/3 速度)
stats.value.hunger = Math.max(0, stats.value.hunger - (hungerDecay * 0.3));
stats.value.happiness = Math.max(0, stats.value.happiness - (happinessDecay * 0.3));
stats.value.hunger = Math.max(0, stats.value.hunger - 0.015);
stats.value.happiness = Math.max(0, stats.value.happiness - 0.025);
}
// Random poop generation (更低的機率:約 0.5% per tick)
@ -198,39 +128,16 @@ export function usePetSystem() {
}
// Sickness Check (更低的生病機率)
// 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) {
if (Math.random() < 0.1) { // 從 0.3 降到 0.1
state.value = 'sick';
}
}
// Health Recovery (健康值可以緩慢恢復)
// 如果沒有便便、飢餓值和快樂值都高,健康值會緩慢恢復
let healthRecovery = 0.05;
// Deity Buff: 觀音 - Health 回復 +20%
if (stats.value.currentDeity === 'guanyin' && stats.value.deityFavors?.guanyin > 0) {
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);
stats.value.health = Math.min(100, stats.value.health + 0.05);
}
// Death Check (移除死亡機制,依照之前的討論)
@ -238,53 +145,9 @@ export function usePetSystem() {
// 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() {
// Simple evolution logic
if (stage.value === 'baby' && stats.value.age >= 3) {
stage.value = 'child';
triggerState('happy', 2000); // Celebrate
} else if (stage.value === 'child' && stats.value.age >= 7) {
stage.value = 'adult';
triggerState('happy', 2000);
}
// Evolution / Growth (Simple Age increment)
// In a real game, 1 day might be 24h, here maybe every 100 ticks?
// For now, let's just say age increases slowly.
}
// --- Helpers ---
@ -308,10 +171,6 @@ export function usePetSystem() {
stats.value.happiness = 50;
stats.value.health = 100;
stats.value.poopCount = 0;
// v2: Assign Destiny
assignDestiny();
isCleaning.value = false;
}
}
@ -325,55 +184,9 @@ export function usePetSystem() {
happiness: 100,
health: 100,
weight: 500,
age: 1,
poopCount: 0,
// v2 Reset
str: 0,
int: 0,
dex: 0,
generation: 1,
deityFavor: 0,
destiny: null
age: 0,
poopCount: 0
};
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 ---
@ -395,11 +208,6 @@ export function usePetSystem() {
clean,
sleep,
hatchEgg,
reset,
achievements,
unlockAllAchievements,
assignDestiny, // Export for debug if needed
resurrect,
reincarnate
reset
};
}