Browse Source

日常开发

林旭祥 1 day ago
parent
commit
a531fcd9ec

BIN
public/static/images/basketball/background.png


BIN
public/static/images/basketball/ball.png


BIN
public/static/images/basketball/bounce_1.wav


BIN
public/static/images/basketball/hoop.png


BIN
public/static/images/basketball/title.png


+ 544 - 8
src/views/game/basketball.vue

@@ -1,23 +1,559 @@
 <template>
-  <div>
-
+  <div class="game-container">
+    <canvas id="canvas" @mousedown="handleMouseDown" @mouseup="handleMouseUp" @touchstart="handleTouchStart"
+      @touchend="handleTouchEnd"></canvas>
   </div>
 </template>
 
-<script setup name="Basketball" lang="ts">
+<script setup>
+import { onMounted, ref, reactive, onBeforeUnmount } from 'vue';
 
+// 游戏主类的响应式状态
+const gameState = reactive({
+  version: '0.1',
+  balls: [],
+  hoops: [],
+  texts: [],
+  res: {},
+  score: 0,
+  started: false,
+  gameOver: false,
+  ballX: 320 / 2,
+  ballY: 880,
+  ballVel: 500,
+  ballAngleVel: 100,
+  ballAngle: 0,
+  ballsShot: 1,
+  ballCharge: 0,
+  time: 60,
+  toNextSecond: 1,
+  sound: false,
+  state: 'menu',
+  click: false,
+  canvas: null,
+  ctx: null,
+  animationFrameId: null,
+  then: 0
+});
 
-onBeforeMount(async () => {
+// 篮筐类
+class Hoop {
+  constructor(x, y) {
+    this.x = x;
+    this.y = y;
+    this.points = [
+      { x: x + 7, y: y + 18 },
+      { x: x + 141, y: y + 18 }
+    ];
+  }
 
-});
+  drawBack(ctx, game) {
+    drawImage(
+      ctx,
+      game.res['/static/images/basketball/hoop.png'],
+      this.x,
+      this.y,
+      0, 0, 148, 22, 0, 0, 0
+    );
+  }
 
-onMounted(() => {
+  drawFront(ctx, game) {
+    drawImage(
+      ctx,
+      game.res['/static/images/basketball/hoop.png'],
+      this.x,
+      this.y + 22,
+      0, 22, 148, 178 - 22, 0, 0, 0
+    );
+  }
+}
+
+// 篮球类
+class Ball {
+  constructor(x, y) {
+    this.x = x;
+    this.y = y;
+    this.vx = 0;
+    this.vy = 0;
+    this.speed = 100;
+    this.canBounce = true;
+    this.angle = 270;
+    this.gravity = 0;
+    this.falling = false;
+    this.bounces = 0;
+    this.scored = false;
+    this.drawAngle = 0;
+    this.angleVel = 100;
+    this.solid = false;
+    this.z = 1;
+  }
+
+  setAngle(angle) {
+    this.angle = angle;
+    this.vx = this.speed * Math.cos(this.angle * Math.PI / 180);
+    this.vy = this.speed * Math.sin(this.angle * Math.PI / 180);
+    this.gravity = 0;
+  }
+
+  shoot(power) {
+    this.speed = power + Math.floor(Math.random() * 40);
+    this.setAngle(270);
+  }
+
+  update(delta) {
+    this.y += this.gravity * delta;
+    this.gravity += 1500 * delta;
+    this.x += this.vx * delta;
+    this.y += this.vy * delta;
+
+    if (this.vx > 500) this.vx = 500;
+    if (this.vy > 500) this.vy = 500;
+
+    if (this.y < 300) {
+      this.solid = true;
+    }
+
+    if (this.gravity > this.speed) {
+      this.falling = true;
+    }
+
+    if (this.x + 47 > 640) {
+      this.vx = this.vx * -1;
+      this.x = 640 - 47;
+    }
+
+    if (this.x - 47 < 0) {
+      this.vx = this.vx * -1;
+      this.x = 47;
+    }
+
+    this.drawAngle += this.angleVel * delta;
+  }
+
+  draw(ctx, game) {
+    drawImage(
+      ctx,
+      game.res['/static/images/basketball/ball.png'],
+      Math.floor(this.x - (93 / 2)),
+      Math.floor(this.y - (93 / 2)),
+      0, 0, 93, 93, 93 / 2, 93 / 2,
+      this.drawAngle
+    );
+  }
+}
+
+// 弹出文字类
+class PopText {
+  constructor(string, x, y) {
+    this.string = string;
+    this.x = x;
+    this.y = y;
+    this.vy = -500;
+    this.opacity = 1;
+  }
+
+  update(delta) {
+    this.y += this.vy * delta;
+    this.vy += 1000 * delta;
+
+    if (this.vy > 0 && this.opacity > 0) {
+      this.opacity -= 2 * delta;
+    }
+
+    if (this.opacity <= 0) {
+      this.opacity = 0;
+    }
+  }
+
+  draw(ctx, game) {
+    ctx.globalAlpha = this.opacity;
+    game.drawText(ctx, this.string, this.x + 15, this.y);
+    ctx.globalAlpha = 1;
+  }
+}
+
+// 工具函数:绘制旋转图像
+function drawImage(ctx, image, x, y, sx, sy, w, h, rx, ry, a) {
+  ctx.save();
+  ctx.translate(x + rx, y + ry);
+  ctx.rotate(a * Math.PI / 180);
+  ctx.drawImage(image, sx, sy, w, h, -rx, -ry, w, h);
+  ctx.restore();
+}
+
+// 事件处理函数
+const handleMouseDown = () => {
+  gameState.click = true;
+};
+
+const handleMouseUp = () => {
+  gameState.click = false;
+};
+
+const handleTouchStart = () => {
+  gameState.click = true;
+};
+
+const handleTouchEnd = () => {
+  gameState.click = false;
+};
+
+// 游戏方法
+const setupCanvas = () => {
+  gameState.canvas = document.getElementById('canvas');
+  gameState.canvas.width = 640;
+  gameState.canvas.height = 960;
+  gameState.ctx = gameState.canvas.getContext('2d');
+};
+
+const resizeToWindow = () => {
+  const w = gameState.canvas.width / gameState.canvas.height;
+  const h = window.innerHeight;
+  const ratio = h * w;
+  gameState.canvas.style.width = Math.floor(ratio) + 'px';
+  gameState.canvas.style.height = Math.floor(h) + 'px';
+};
+
+const drawLoadingScreen = () => {
+  const ctx = gameState.ctx;
+  ctx.fillStyle = 'black';
+  ctx.fillRect(0, 0, 960, 640);
+  ctx.textAlign = 'center';
+  drawText(ctx, 'Loading...', 640 / 2, 960 / 2, 40);
+  ctx.textAlign = 'left';
+};
+
+const getResources = () => {
+  const images = [
+    '/static/images/basketball/background.png',
+    '/static/images/basketball/title.png',
+    '/static/images/basketball/ball.png',
+    '/static/images/basketball/hoop.png'
+  ];
+
+  const sounds = [
+    '/static/images/basketball/bounce_1.wav'
+  ];
+
+  return gameState.sound ? images.concat(sounds) : images;
+};
+
+const drawText = (ctx, string, x, y, size = 30) => {
+  ctx.font = size + 'px Contrail One';
+  ctx.lineWidth = 5;
+  ctx.strokeStyle = 'white';
+  ctx.strokeText(string, x, y);
+  ctx.fillStyle = '#0098BF';
+  ctx.fillText(string, x, y);
+};
+
+const playSound = (name) => {
+  if (gameState.sound && gameState.res[name]) {
+    gameState.res[name].currentTime = 0;
+    gameState.res[name].play().catch(e => console.log('Sound play error:', e));
+  }
+};
+
+const loadResources = () => {
+  drawLoadingScreen();
+  const resources = getResources();
+  let loaded = 0;
+
+  return new Promise((resolve) => {
+    resources.forEach(resource => {
+      const type = resource.split('.').pop();
+
+      if (type === 'png') {
+        const image = new Image();
+        image.src = resource;
+        image.addEventListener('load', () => {
+          loaded++;
+          gameState.res[resource] = image;
+          if (loaded === resources.length) resolve();
+        });
+      } else if (['wav', 'mp3'].includes(type)) {
+        const sound = new Audio();
+        sound.src = resource;
+        sound.addEventListener('canplaythrough', () => {
+          loaded++;
+          gameState.res[resource] = sound;
+          if (loaded === resources.length) resolve();
+        });
+      }
+    });
+  });
+};
+
+const gameLoop = (timestamp) => {
+  if (!gameState.then) gameState.then = timestamp;
+  const delta = (timestamp - gameState.then) / 1000;
+
+  // 更新游戏状态
+  update(delta);
+  // 绘制游戏
+  draw();
 
+  gameState.then = timestamp;
+  gameState.animationFrameId = requestAnimationFrame(gameLoop);
+};
+
+const update = (delta) => {
+  if (gameState.state === 'menu') {
+    if (gameState.click) {
+      gameState.state = 'play';
+      gameState.click = false;
+    }
+    return;
+  }
+
+  if (gameState.state === 'play') {
+    // 更新篮球横向移动
+    gameState.ballX += gameState.ballVel * delta;
+    if (gameState.ballX > 640 - 93) {
+      gameState.ballVel = -gameState.ballVel;
+      gameState.ballX = 640 - 93;
+    }
+    if (gameState.ballX < 0) {
+      gameState.ballVel = -gameState.ballVel;
+      gameState.ballX = 0;
+    }
+
+    // 更新所有篮球
+    for (let i = gameState.balls.length - 1; i >= 0; i--) {
+      const ball = gameState.balls[i];
+
+      if (ball.falling) {
+        // 检测与篮筐的碰撞
+        gameState.hoops.forEach(hoop => {
+          const cx = hoop.x + (148 / 2);
+          const cy = hoop.y + 40;
+          const dx = cx - ball.x;
+          const dy = cy - ball.y;
+          const mag = Math.sqrt(dx * dx + dy * dy);
+
+          if (mag < 47 + 5 && !ball.scored) {
+            ball.setAngle(90);
+            gameState.score += 100;
+            gameState.texts.push(new PopText('+ 100', hoop.x, hoop.y));
+            ball.scored = true;
+          }
+
+          if (!ball.scored) {
+            hoop.points.forEach(point => {
+              const dx = point.x - ball.x;
+              const dy = point.y - ball.y;
+              const mag = Math.sqrt(dx * dx + dy * dy);
+              const angle = Math.atan2(point.y - ball.y, point.x - ball.x);
+
+              if (mag > 47 + 7 && !ball.canBounce) {
+                ball.canBounce = true;
+              }
+
+              if (mag < 47 + 5 && ball.canBounce) {
+                playSound('/static/images/basketball/bounce_1.wav');
+                ball.bounces++;
+                ball.setAngle((angle * 180 / Math.PI) + 180 + Math.floor(Math.random() * 5) - Math.floor(Math.random() * 5));
+                ball.bounces = Math.min(ball.bounces, 3);
+
+                const deg = angle * 180 / Math.PI;
+                if (deg > 0 && deg < 180) {
+                  ball.gravity = 750 + (ball.bounces * 50);
+                }
+
+                ball.angleVel = -ball.angleVel;
+                ball.canBounce = false;
+              }
+            });
+          }
+        });
+      }
+
+      ball.update(delta);
+
+      // 移除超出屏幕的球
+      if (ball.y > 960) {
+        gameState.ballX = ball.x;
+        gameState.balls.splice(i, 1);
+      }
+    }
+
+    // 更新计时器
+    if (gameState.time > 0) {
+      gameState.toNextSecond -= delta;
+      if (gameState.toNextSecond <= 0) {
+        gameState.time--;
+        gameState.toNextSecond = 1;
+      }
+    } else {
+      gameState.state = 'over';
+    }
+
+    // 处理投篮逻辑
+    if (gameState.click && gameState.ballY <= 950) {
+      if (gameState.balls.length < 1) {
+        const ball = new Ball(gameState.ballX + (93 / 2), gameState.ballY);
+        ball.drawAngle = gameState.ballAngle;
+        ball.shoot(1480);
+        gameState.balls.push(ball);
+        gameState.ballY = 961;
+      }
+    }
+
+    // 重置篮球位置
+    if (gameState.balls.length < 1 && gameState.ballY > 880) {
+      gameState.ballY -= 100 * delta;
+    }
+
+    if (!gameState.click) {
+      gameState.ballsShot = 0;
+    }
+
+    // 更新弹出文字
+    gameState.texts.forEach((text, i) => {
+      text.update(delta);
+      if (text.opacity <= 0) {
+        gameState.texts.splice(i, 1);
+      }
+    });
+
+    // 更新篮球角度
+    gameState.ballAngle += 100 * delta;
+  }
+
+  // 游戏结束状态处理
+  if (gameState.state === 'over' && gameState.click) {
+    gameState.gameOver = false;
+    gameState.started = false;
+    gameState.score = 0;
+    gameState.time = 60;
+    gameState.balls = [];
+    gameState.state = 'menu';
+    gameState.click = false;
+  }
+};
+
+const draw = () => {
+  const ctx = gameState.ctx;
+  if (!ctx) return;
+
+  // 绘制背景
+  if (gameState.res['/static/images/basketball/background.png']) {
+    ctx.drawImage(gameState.res['/static/images/basketball/background.png'], 0, 0);
+  }
+
+  // 绘制菜单状态
+  if (gameState.state === 'menu') {
+    if (gameState.res['/static/images/basketball/title.png']) {
+      ctx.drawImage(
+        gameState.res['/static/images/basketball/title.png'],
+        640 / 2 - (492 / 2),
+        100
+      );
+    }
+    ctx.textAlign = 'center';
+    drawText(ctx, 'Click / Touch to Start!', 640 / 2, 520, 40);
+    drawText(ctx, '(?) Tap to throw ball. Try to make as many hoops before the time runs out!', 640 / 2, 940, 20);
+    ctx.textAlign = 'left';
+  }
+
+  // 绘制游戏状态
+  if (gameState.state === 'play') {
+    // 绘制篮筐背景
+    gameState.hoops.forEach(hoop => hoop.drawBack(ctx, gameState));
+
+    // 绘制正在下落的球
+    gameState.balls.forEach(ball => {
+      if (ball.falling) ball.draw(ctx, gameState);
+    });
+
+    // 绘制篮筐前景
+    gameState.hoops.forEach(hoop => hoop.drawFront(ctx, gameState));
+
+    // 绘制未下落的球
+    gameState.balls.forEach(ball => {
+      if (!ball.falling) ball.draw(ctx, gameState);
+    });
+
+    // 绘制当前可投掷的球
+    if (gameState.balls.length < 1 && gameState.res['/static/images/basketball/ball.png']) {
+      drawImage(
+        ctx,
+        gameState.res['/static/images/basketball/ball.png'],
+        gameState.ballX,
+        gameState.ballY,
+        0, 0, 93, 93,
+        45, 45,
+        gameState.ballAngle
+      );
+    }
+
+    // 绘制分数和时间
+    drawText(ctx, 'Score: ' + gameState.score, 45, 70, 50);
+    drawText(ctx, 'Time: ' + gameState.time, 435, 70, 50);
+
+    // 绘制弹出文字
+    gameState.texts.forEach(text => text.draw(ctx, { drawText }));
+  }
+
+  // 绘制游戏结束界面
+  if (gameState.state === 'over') {
+    ctx.textAlign = 'center';
+    drawText(ctx, 'Game Over', 640 / 2, 200, 80);
+    drawText(ctx, 'Score: ' + gameState.score, 640 / 2, 400, 50);
+    drawText(ctx, 'Click to Continue', 640 / 2, 800, 50);
+    ctx.textAlign = 'center';
+  }
+};
+
+// 初始化游戏
+const initGame = async () => {
+  setupCanvas();
+  resizeToWindow();
+  window.addEventListener('resize', resizeToWindow);
+
+  await loadResources();
+
+  // 添加篮筐
+  gameState.hoops = [
+    new Hoop(110, 520),
+    new Hoop(640 - 148 - 110, 520),
+    new Hoop(640 / 2 - (148 / 2), 260)
+  ];
+
+  // 开始游戏循环
+  gameState.animationFrameId = requestAnimationFrame(gameLoop);
+};
+
+// 生命周期钩子
+onMounted(() => {
+  initGame();
+  setInterval(()=>{
+    handleMouseDown()
+  },500)
 });
 
 onBeforeUnmount(() => {
-
+  if (gameState.animationFrameId) {
+    cancelAnimationFrame(gameState.animationFrameId);
+  }
+  window.removeEventListener('resize', resizeToWindow);
 });
 </script>
 
-<style lang="scss" scoped></style>
+<style scoped>
+.game-container {
+  width: 100vw;
+  height: 100vh;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background-color: #000;
+  overflow: hidden;
+}
+
+#canvas {
+  display: block;
+  touch-action: none;
+  user-select: none;
+}
+</style>