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"
|
v-if="showPetInfo"
|
||||||
:petName="CURRENT_PRESET.name"
|
:petName="CURRENT_PRESET.name"
|
||||||
:stage="stage"
|
:stage="stage"
|
||||||
:poopCount="poopCount"
|
:poopCount="stats?.poopCount || 0"
|
||||||
:baseStats="baseStats"
|
:baseStats="baseStats"
|
||||||
:hunger="stats?.hunger || 100"
|
:hunger="stats?.hunger || 100"
|
||||||
:happiness="stats?.happiness || 100"
|
:happiness="stats?.happiness || 100"
|
||||||
|
|
@ -212,6 +212,20 @@
|
||||||
@close="currentGame = ''"
|
@close="currentGame = ''"
|
||||||
@complete="handleGameComplete"
|
@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 -->
|
<!-- Inventory Screen -->
|
||||||
<InventoryScreen
|
<InventoryScreen
|
||||||
|
|
@ -255,6 +269,8 @@ import PetInfoScreen from './PetInfoScreen.vue';
|
||||||
import InventoryScreen from './InventoryScreen.vue';
|
import InventoryScreen from './InventoryScreen.vue';
|
||||||
import PlayMenu from './PlayMenu.vue';
|
import PlayMenu from './PlayMenu.vue';
|
||||||
import GuessingGame from './GuessingGame.vue';
|
import GuessingGame from './GuessingGame.vue';
|
||||||
|
import TrainingGame from './TrainingGame.vue';
|
||||||
|
import BallGame from './BallGame.vue';
|
||||||
import guanyinLots from '../assets/guanyin_100_lots.json';
|
import guanyinLots from '../assets/guanyin_100_lots.json';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -365,18 +381,85 @@ function handleJiaobeiClose() {
|
||||||
function handlePlaySelect(gameType) {
|
function handlePlaySelect(gameType) {
|
||||||
showPlayMenu.value = false;
|
showPlayMenu.value = false;
|
||||||
console.log('Selected game:', gameType);
|
console.log('Selected game:', gameType);
|
||||||
|
console.log('Setting currentGame to:', gameType);
|
||||||
|
|
||||||
// Show the selected game
|
// Show the selected game
|
||||||
currentGame.value = gameType;
|
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) {
|
function handleGameComplete(won) {
|
||||||
console.log('Game completed, won:', won);
|
console.log('Game completed, won:', won);
|
||||||
currentGame.value = '';
|
currentGame.value = '';
|
||||||
|
|
||||||
if (won) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Training mode: pet stays in position
|
||||||
|
if (currentGame.value === 'training') return;
|
||||||
|
|
||||||
console.log('moveRandomly called. State:', props.state);
|
console.log('moveRandomly called. State:', props.state);
|
||||||
if (props.state === 'sleep' || props.state === 'dead' || props.state === 'eating' || isMedicineActive.value) {
|
if (props.state === 'sleep' || props.state === 'dead' || props.state === 'eating' || isMedicineActive.value) {
|
||||||
updateHeadIconsPosition();
|
updateHeadIconsPosition();
|
||||||
|
|
@ -1711,6 +1797,17 @@ defineExpose({
|
||||||
background: #fff;
|
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 {
|
.event-animation.float-up {
|
||||||
animation: floatUp 2s ease-out forwards;
|
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