182 lines
4.8 KiB
Vue
182 lines
4.8 KiB
Vue
<script setup>
|
||
// 下100层:不断下落踩稳平台,顶部尖刺不能碰,层数即分数
|
||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||
import { createLoop, Keys, palette, randInt } from '../engine'
|
||
|
||
const emit = defineEmits(['score', 'end'])
|
||
const canvas = ref(null)
|
||
const W = 480
|
||
const H = 560
|
||
let ctx
|
||
let keys
|
||
let loop
|
||
let player = { x: W / 2, y: 100, vy: 0, onPlat: null }
|
||
let platforms = [] // {x,y,w,type: normal|spike|move, dir}
|
||
let cameraY = 0 // 摄像机向下滚动
|
||
let floor = 0 // 已下的层数(分数)
|
||
let alive = false
|
||
let scrollSpeed = 40 // 画面自动上移速度
|
||
|
||
function spawnPlatform(y) {
|
||
const roll = Math.random()
|
||
const type = roll < 0.68 ? 'normal' : roll < 0.85 ? 'move' : 'spike'
|
||
platforms.push({
|
||
x: randInt(20, W - 120),
|
||
y,
|
||
w: randInt(80, 140),
|
||
type,
|
||
dir: Math.random() < 0.5 ? 1 : -1,
|
||
})
|
||
}
|
||
|
||
function update(dt) {
|
||
// 画面自动上滚(越来越快)
|
||
scrollSpeed = 40 + floor * 0.9
|
||
cameraY += scrollSpeed * dt
|
||
// 左右移动
|
||
const ms = 300
|
||
if (keys.has('ArrowLeft') || keys.has('a')) player.x -= ms * dt
|
||
if (keys.has('ArrowRight') || keys.has('d')) player.x += ms * dt
|
||
player.x = Math.max(14, Math.min(W - 14, player.x))
|
||
// 重力
|
||
player.vy += 1300 * dt
|
||
const prevY = player.y
|
||
player.y += player.vy * dt
|
||
// 平台碰撞(只在下落时检测顶面)
|
||
player.onPlat = null
|
||
for (const p of platforms) {
|
||
if (player.vy >= 0 && prevY <= p.y - 10 + 1 && player.y >= p.y - 10 && player.x > p.x - 10 && player.x < p.x + p.w + 10) {
|
||
if (p.type === 'spike') {
|
||
// 尖刺平台:踩上即死
|
||
die()
|
||
return
|
||
}
|
||
player.y = p.y - 10
|
||
player.vy = 0
|
||
player.onPlat = p
|
||
}
|
||
}
|
||
// 移动平台带着玩家走
|
||
platforms.forEach((p) => {
|
||
if (p.type === 'move') {
|
||
p.x += p.dir * 90 * dt
|
||
if (p.x < 10 || p.x + p.w > W - 10) p.dir *= -1
|
||
if (player.onPlat === p) player.x += p.dir * 90 * dt
|
||
}
|
||
})
|
||
// 计层:每通过一块平台的高度 +1 层
|
||
const newFloor = Math.floor((player.y - 160) / 90)
|
||
if (newFloor > floor) {
|
||
floor = newFloor
|
||
emit('score', floor)
|
||
}
|
||
// 生成更深的平台 / 清理头顶的
|
||
const deepest = Math.max(...platforms.map((p) => p.y))
|
||
if (deepest < cameraY + H + 200) spawnPlatform(deepest + 90)
|
||
platforms = platforms.filter((p) => p.y > cameraY - 60)
|
||
// 死亡判定:被顶出屏幕上沿(被卷进尖刺天花板)或掉出屏幕下沿
|
||
if (player.y < cameraY + 24 || player.y > cameraY + H + 40) {
|
||
die()
|
||
return
|
||
}
|
||
draw()
|
||
}
|
||
|
||
function die() {
|
||
alive = false
|
||
loop.stop()
|
||
draw()
|
||
emit('end', { score: floor })
|
||
}
|
||
|
||
function draw() {
|
||
const p = palette()
|
||
ctx.fillStyle = p.bg
|
||
ctx.fillRect(0, 0, W, H)
|
||
// 顶部尖刺天花板
|
||
ctx.fillStyle = p.primary
|
||
for (let x = 0; x < W; x += 24) {
|
||
ctx.beginPath()
|
||
ctx.moveTo(x, 0)
|
||
ctx.lineTo(x + 12, 18)
|
||
ctx.lineTo(x + 24, 0)
|
||
ctx.fill()
|
||
}
|
||
// 平台
|
||
platforms.forEach((pl) => {
|
||
const sy = pl.y - cameraY
|
||
if (sy < -20 || sy > H + 20) return
|
||
if (pl.type === 'spike') {
|
||
ctx.fillStyle = p.primary
|
||
for (let x = pl.x; x < pl.x + pl.w - 10; x += 14) {
|
||
ctx.beginPath()
|
||
ctx.moveTo(x, sy)
|
||
ctx.lineTo(x + 7, sy - 12)
|
||
ctx.lineTo(x + 14, sy)
|
||
ctx.fill()
|
||
}
|
||
ctx.fillStyle = '#8b3a44'
|
||
ctx.fillRect(pl.x, sy, pl.w, 10)
|
||
} else {
|
||
ctx.fillStyle = pl.type === 'move' ? p.accent : p.primary2
|
||
ctx.beginPath()
|
||
ctx.roundRect(pl.x, sy, pl.w, 10, 5)
|
||
ctx.fill()
|
||
}
|
||
})
|
||
// 玩家
|
||
ctx.font = '26px serif'
|
||
ctx.textAlign = 'center'
|
||
ctx.textBaseline = 'bottom'
|
||
ctx.fillText('🧗', player.x, player.y - cameraY + 4)
|
||
// 层数
|
||
ctx.fillStyle = p.text
|
||
ctx.font = 'bold 18px sans-serif'
|
||
ctx.textAlign = 'left'
|
||
ctx.textBaseline = 'top'
|
||
ctx.fillText(`第 ${floor} 层`, 10, 26)
|
||
}
|
||
|
||
// ---- 对外接口 ----
|
||
function start() {
|
||
player = { x: W / 2, y: 120, vy: 0, onPlat: null }
|
||
platforms = [{ x: W / 2 - 70, y: 180, w: 140, type: 'normal', dir: 1 }]
|
||
for (let y = 270; y < H + 300; y += 90) spawnPlatform(y)
|
||
cameraY = 0
|
||
floor = 0
|
||
alive = true
|
||
emit('score', 0)
|
||
loop.start()
|
||
}
|
||
function stop() {
|
||
loop?.stop()
|
||
keys?.detach()
|
||
}
|
||
// 复活卡:回到屏幕中间的安全平台继续
|
||
function useProp(code) {
|
||
if (code === 'revive' && !alive) {
|
||
const safe = { x: W / 2 - 70, y: cameraY + H / 2, w: 140, type: 'normal', dir: 1 }
|
||
platforms.push(safe)
|
||
player = { x: W / 2, y: safe.y - 10, vy: 0, onPlat: safe }
|
||
alive = true
|
||
loop.start()
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
defineExpose({ start, stop, useProp })
|
||
|
||
onMounted(() => {
|
||
ctx = canvas.value.getContext('2d')
|
||
keys = new Keys()
|
||
keys.attach()
|
||
loop = createLoop(update)
|
||
draw()
|
||
})
|
||
onBeforeUnmount(stop)
|
||
</script>
|
||
|
||
<template>
|
||
<canvas ref="canvas" :width="W" :height="H"></canvas>
|
||
</template>
|