good version
This commit is contained in:
parent
11c01fb1ea
commit
1c068b1672
|
|
@ -0,0 +1,307 @@
|
|||
<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>
|
||||
|
|
@ -190,7 +190,7 @@
|
|||
v-if="showPetInfo"
|
||||
:petName="CURRENT_PRESET.name"
|
||||
:stage="stage"
|
||||
:poopCount="poopCount"
|
||||
:poopCount="stats?.poopCount || 0"
|
||||
:baseStats="baseStats"
|
||||
:hunger="stats?.hunger || 100"
|
||||
:happiness="stats?.happiness || 100"
|
||||
|
|
@ -213,6 +213,20 @@
|
|||
@complete="handleGameComplete"
|
||||
/>
|
||||
|
||||
<TrainingGame
|
||||
v-if="currentGame === 'training'"
|
||||
@close="currentGame = ''"
|
||||
@complete="handleGameComplete"
|
||||
@attack="handleTrainingAttack"
|
||||
/>
|
||||
|
||||
<BallGame
|
||||
v-if="currentGame === 'ball'"
|
||||
@close="currentGame = ''"
|
||||
@complete="handleBallGameComplete"
|
||||
@updatePetX="handleBallGameUpdate"
|
||||
/>
|
||||
|
||||
<!-- Inventory Screen -->
|
||||
<InventoryScreen
|
||||
v-if="showInventory"
|
||||
|
|
@ -255,6 +269,8 @@ import PetInfoScreen from './PetInfoScreen.vue';
|
|||
import InventoryScreen from './InventoryScreen.vue';
|
||||
import PlayMenu from './PlayMenu.vue';
|
||||
import GuessingGame from './GuessingGame.vue';
|
||||
import TrainingGame from './TrainingGame.vue';
|
||||
import BallGame from './BallGame.vue';
|
||||
import guanyinLots from '../assets/guanyin_100_lots.json';
|
||||
|
||||
const props = defineProps({
|
||||
|
|
@ -365,18 +381,85 @@ function handleJiaobeiClose() {
|
|||
function handlePlaySelect(gameType) {
|
||||
showPlayMenu.value = false;
|
||||
console.log('Selected game:', gameType);
|
||||
console.log('Setting currentGame to:', gameType);
|
||||
|
||||
// Show the selected game
|
||||
currentGame.value = gameType;
|
||||
}
|
||||
|
||||
// Watch for game changes to position pet
|
||||
watch(currentGame, (newGame) => {
|
||||
if (newGame === 'training') {
|
||||
// Move pet to right side for training
|
||||
if (containerRef.value) {
|
||||
const cw = containerRef.value.clientWidth;
|
||||
const ch = containerRef.value.clientHeight;
|
||||
// Position on right side (approx 260px for 300px width)
|
||||
petX.value = cw - 60;
|
||||
petY.value = ch - height - 20; // Bottom aligned with padding
|
||||
// 根據使用者的反饋,這裡調整面向
|
||||
// 如果原本 false 是錯的,那我們改成 true
|
||||
isFacingRight.value = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function handleTrainingAttack() {
|
||||
// Trigger attack animation (open mouth)
|
||||
isMouthOpen.value = true;
|
||||
setTimeout(() => {
|
||||
isMouthOpen.value = false;
|
||||
}, 300);
|
||||
}
|
||||
console.log('currentGame is now:', currentGame.value);
|
||||
|
||||
|
||||
function handleGameComplete(won) {
|
||||
console.log('Game completed, won:', won);
|
||||
currentGame.value = '';
|
||||
|
||||
if (won) {
|
||||
// Reward: increase happiness
|
||||
emit('action', 'play');
|
||||
// 訓練完成,觸發開心事件
|
||||
triggerState('happy', 3000);
|
||||
|
||||
// 顯示頭頂音符動畫
|
||||
eventAnimation.value = {
|
||||
type: 'float-up',
|
||||
iconClass: 'pixel-note'
|
||||
};
|
||||
setTimeout(() => {
|
||||
eventAnimation.value = null;
|
||||
}, 2000);
|
||||
|
||||
// 增加一些數值作為獎勵
|
||||
stats.value.happiness = Math.min(100, stats.value.happiness + 10);
|
||||
stats.value.hunger = Math.max(0, stats.value.hunger - 5); // 運動會餓
|
||||
}
|
||||
}
|
||||
|
||||
function handleBallGameUpdate(x) {
|
||||
petX.value = x;
|
||||
// 根據移動方向改變面向 (簡單判斷:如果 x 變大則向右,變小則向左)
|
||||
// 這裡 BallGame 已經處理了邏輯,我們只需要更新位置
|
||||
}
|
||||
|
||||
function handleBallGameComplete(won) {
|
||||
console.log('Ball game completed, won:', won);
|
||||
currentGame.value = '';
|
||||
|
||||
if (won) {
|
||||
triggerState('happy', 3000);
|
||||
// 顯示頭頂音符動畫
|
||||
eventAnimation.value = {
|
||||
type: 'float-up',
|
||||
iconClass: 'pixel-note'
|
||||
};
|
||||
setTimeout(() => {
|
||||
eventAnimation.value = null;
|
||||
}, 2000);
|
||||
|
||||
stats.value.happiness = Math.min(100, stats.value.happiness + 15);
|
||||
stats.value.hunger = Math.max(0, stats.value.hunger - 10);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -862,6 +945,9 @@ function moveRandomly() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Training mode: pet stays in position
|
||||
if (currentGame.value === 'training') return;
|
||||
|
||||
console.log('moveRandomly called. State:', props.state);
|
||||
if (props.state === 'sleep' || props.state === 'dead' || props.state === 'eating' || isMedicineActive.value) {
|
||||
updateHeadIconsPosition();
|
||||
|
|
@ -1711,6 +1797,17 @@ defineExpose({
|
|||
background: #fff;
|
||||
}
|
||||
|
||||
.pixel-note {
|
||||
box-shadow:
|
||||
1px -2px #000, 2px -2px #000,
|
||||
1px -1px #000, 2px -1px #000, 3px -1px #000,
|
||||
1px 0px #000, 3px 0px #000,
|
||||
1px 1px #000, 3px 1px #000,
|
||||
-2px 2px #000, -1px 2px #000, 1px 2px #000, 3px 2px #000,
|
||||
-2px 3px #000, -1px 3px #000, 1px 3px #000, 3px 3px #000;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.event-animation.float-up {
|
||||
animation: floatUp 2s ease-out forwards;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,330 @@
|
|||
<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>
|
||||
Loading…
Reference in New Issue