🎮ArcadeLab

敦煌寻星 · 壁画逃影

by GoldenPilot43
583 lines15.3 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>敦煌寻星 · 壁画逃影</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
body{
    background:#271f17;
    font-family:"SimHei","Microsoft YaHei",serif;
    color:#f0ddbc;
    padding:10px;
    overflow:hidden;
    background-image:
        radial-gradient(circle at 20% 30%, #5c4838 1px, transparent 1px),
        radial-gradient(circle at 80% 70%, #3d3024 1px, transparent 1px);
    background-size:22px 22px, 18px 18px;
}
.title{
    text-align:center;
    font-size:22px;
    color:#f5d76e;
    letter-spacing:6px;
    margin-bottom:4px;
    text-shadow:0 0 10px #d4af3780;
}
.subtitle{
    text-align:center;
    font-size:13px;
    color:#c9b38f;
    margin-bottom:8px;
    letter-spacing:2px;
}
.info{
    text-align:center;
    font-size:17px;
    margin-bottom:8px;
    color:#f0ddbc;
}
.info b{color:#f5d76e}
#gameBox{
    width:100%;
    max-width:440px;
    height:340px;
    background:#4c3a2c;
    border:4px solid #d4af37;
    border-radius:10px;
    position:relative;
    overflow:hidden;
    margin:0 auto;
    box-shadow:
        inset 0 0 60px #2b2018,
        0 0 20px #00000060;
}

/* 藻井顶部 */
.ceiling{
    position:absolute;
    top:0;left:0;right:0;
    height:56px;
    background:
        radial-gradient(ellipse at center, #d4af3722 0%, transparent 70%),
        linear-gradient(180deg, #3a2c20 0%, #4c3a2c 100%);
    border-bottom:2px solid #d4af3755;
    z-index:1;
}
.ceiling-pattern{
    position:absolute;
    inset:0;
    background-image:
        repeating-conic-gradient(from 0deg, #d4af3733 0 10deg, transparent 10deg 20deg);
    opacity:0.35;
    mask-image:radial-gradient(ellipse at center, black 40%, transparent 75%);
}

/* 壁画背景层 */
.mural{
    position:absolute;
    inset:0;
    background:
        radial-gradient(circle at 20% 40%, #7a4a2a55, transparent 40%),
        radial-gradient(circle at 80% 60%, #2f556655, transparent 45%),
        linear-gradient(180deg, #4c3a2c, #3a2c20);
    z-index:0;
}
.mural::before,
.mural::after{
    content:"";
    position:absolute;
    background-repeat:no-repeat;
    opacity:0.35;
}
.mural::before{
    inset:40px 10px auto 10px;
    height:80px;
    background:
        radial-gradient(ellipse at 30% 50%, #f5d76e44 0%, transparent 50%),
        radial-gradient(ellipse at 70% 50%, #d4af3733 0%, transparent 50%);
    border-top:1px dashed #d4af3755;
    border-bottom:1px dashed #d4af3755;
}
.mural::after{
    left:10%;right:10%;bottom:40px;
    height:60px;
    background:
        linear-gradient(90deg, transparent, #9c6b3a44, transparent);
}

/* 卷草纹边框 */
.border-pattern{
    position:absolute;
    left:8px;right:8px;bottom:8px;
    height:18px;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='18' viewBox='0 0 120 18'%3E%3Cpath d='M0,9 C15,0 25,18 40,9 C55,0 65,18 80,9 C95,0 105,18 120,9' stroke='%23d4af37' fill='none' stroke-width='1.2' opacity='0.55'/%3E%3C/svg%3E");
    background-repeat:repeat-x;
    z-index:2;
    pointer-events:none;
}

/* 飞天飘带 */
.feitian{
    position:absolute;
    width:120px;
    height:14px;
    background:linear-gradient(90deg, transparent, #f5d76e, #d4af37, #f5d76e, transparent);
    border-radius:50%;
    filter:blur(1px);
    opacity:0.55;
    animation:flyby linear infinite;
    z-index:3;
    pointer-events:none;
}
.feitian.f1{top:80px;left:-140px;animation-duration:9s}
.feitian.f2{top:140px;left:-140px;animation-duration:11s;animation-delay:-4s;background:linear-gradient(90deg, transparent, #e6b5c7, #f5d76e, #e6b5c7, transparent)}
.feitian.f3{top:210px;left:-140px;animation-duration:13s;animation-delay:-7s;background:linear-gradient(90deg, transparent, #8fc4d6, #f5d76e, #8fc4d6, transparent)}
@keyframes flyby{
    0%{transform:translateX(0) rotate(-4deg)}
    100%{transform:translateX(520px) rotate(4deg)}
}

/* 九色鹿玩家 */
.player{
    width:40px;
    height:32px;
    position:absolute;
    z-index:5;
    filter:drop-shadow(0 0 10px #ffdd77);
    transition:none;
}
.player svg{width:100%;height:100%}

/* 莲花宝珠 */
.star{
    width:26px;
    height:26px;
    position:absolute;
    z-index:4;
    animation:twinkle 1.2s infinite alternate;
    filter:drop-shadow(0 0 8px #fff2aa);
}
.star svg{width:100%;height:100%}

/* 金光特效 */
.sparkle{
    position:absolute;
    width:80px;
    height:80px;
    border-radius:50%;
    background:radial-gradient(circle, #fff2aa 0%, #f5d76e 30%, transparent 70%);
    opacity:0;
    pointer-events:none;
    z-index:6;
}
.sparkle.show{animation:sparkle 0.6s ease-out forwards}
@keyframes sparkle{
    0%{transform:scale(0.2);opacity:0.9}
    100%{transform:scale(1.4);opacity:0}
}

/* 风沙暗影 */
.shadow{
    width:40px;
    height:40px;
    background:
        radial-gradient(circle at 30% 30%, #4a2f22 0%, #1a1410 60%, #000 100%);
    border-radius:50% 45% 55% 40%;
    position:absolute;
    z-index:4;
    box-shadow:0 0 18px #000000cc;
}

/* 风沙颗粒 */
.sand{
    position:absolute;
    width:4px;
    height:4px;
    background:#d4af37;
    border-radius:50%;
    opacity:0.35;
    pointer-events:none;
    z-index:3;
}

@keyframes twinkle{
    from{transform:scale(0.82);opacity:0.65}
    to{transform:scale(1.15);opacity:1}
}

/* 游戏结束层 */
.win,.lose{
    position:absolute;
    inset:0;
    background:rgba(20,14,8,0.88);
    display:none;
    flex-direction:column;
    align-items:center;
    justify-content:center;
    font-size:20px;
    gap:14px;
    text-align:center;
    padding:16px;
    color:#f0ddbc;
    z-index:10;
    border-radius:6px;
}
.win{color:#ffe699}
.win .big,.lose .big{
    font-size:24px;
    color:#f5d76e;
    letter-spacing:4px;
    margin-bottom:6px;
}
button{
    padding:10px 22px;
    background:linear-gradient(180deg,#f5d76e,#d4af37);
    color:#271f17;
    border:2px solid #f5d76e;
    border-radius:6px;
    font-size:16px;
    font-weight:bold;
    letter-spacing:2px;
    cursor:pointer;
}

/* 虚拟摇杆 */
#joystickWrap{
    width:140px;height:140px;
    background:rgba(212,175,55,0.15);
    border:2px solid #d4af37;
    border-radius:50%;
    position:fixed;
    bottom:20px;
    left:20px;
    touch-action:none;
    z-index:20;
}
#joystick{
    width:54px;height:54px;
    background:radial-gradient(circle at 30% 30%, #ffe699, #d4af37 70%);
    border-radius:50%;
    position:absolute;
    left:50%;top:50%;
    transform:translate(-50%,-50%);
    box-shadow:0 0 14px #d4af3799;
}

/* 关卡信息 */
.level-info{
    position:absolute;
    top:62px;right:10px;
    background:#271f1799;
    border:1px solid #d4af3777;
    padding:4px 10px;
    border-radius:20px;
    font-size:12px;
    color:#f5d76e;
    z-index:3;
}
</style>
</head>
<body>

<div class="title">敦煌寻星</div>
<div class="subtitle">壁 画 逃 影</div>
<div class="info">莲花宝珠:<b id="score">0</b> / <span id="total">6</span> 波次:<b id="wave">1</b></div>

<div id="gameBox">
    <div class="ceiling"><div class="ceiling-pattern"></div></div>
    <div class="mural"></div>
    <div class="border-pattern"></div>

    <div class="feitian f1"></div>
    <div class="feitian f2"></div>
    <div class="feitian f3"></div>

    <div class="level-info">守护洞窟</div>

    <div class="player" id="me">
        <svg viewBox="0 0 40 32">
            <path fill="#f8d470" d="M36,16 C32,10 26,8 20,10 C14,12 8,18 6,22 L10,20 L14,22 L20,21 L26,23 L30,20 L34,22 Z"/>
            <circle cx="31" cy="13" r="1.4" fill="#271f17"/>
            <path fill="#fff2aa" d="M34,14 L36,12 L38,14 L36,16 Z" opacity="0.8"/>
        </svg>
    </div>

    <div class="win" id="winBox">
        <div class="big">洞窟重光</div>
        <div>莲花宝珠归位,壁画纹样重新绽放。</div>
        <div>九色鹿守护莫高千年安宁。</div>
        <button onclick="restart()">再来一局</button>
    </div>
    <div class="lose" id="loseBox">
        <div class="big">风 沙 侵 蚀</div>
        <div>暗影吞没了壁画纹样……</div>
        <div>但守护之心永不褪色。</div>
        <button onclick="restart()">重新挑战</button>
    </div>
</div>

<div id="joystickWrap">
    <div id="joystick"></div>
</div>

<script>
const box = document.getElementById('gameBox')
const me = document.getElementById('me')
const scoreDom = document.getElementById('score')
const totalDom = document.getElementById('total')
const waveDom = document.getElementById('wave')
const winBox = document.getElementById('winBox')
const loseBox = document.getElementById('loseBox')
const joyWrap = document.getElementById('joystickWrap')
const joy = document.getElementById('joystick')

let W = box.clientWidth
let H = box.clientHeight
let x = 40, y = 40
let stars = []
let shadows = []
let particles = []
let collected = 0
let totalStar = 6
let wave = 1
let maxWave = 5
let vx = 0, vy = 0
const maxSpeed = 5.2

// 九色鹿朝向
function updateFacing(){
    if(vx > 0.3) me.style.transform = "scaleX(1)"
    if(vx < -0.3) me.style.transform = "scaleX(-1)"
}

// 摇杆
let isTouch = false
let wrapRect, wrapCX, wrapCY
joyWrap.addEventListener('touchstart', e => {
    e.preventDefault()
    isTouch = true
    wrapRect = joyWrap.getBoundingClientRect()
    wrapCX = wrapRect.left + wrapRect.width / 2
    wrapCY = wrapRect.top + wrapRect.height / 2
})
joyWrap.addEventListener('touchmove', e => {
    e.preventDefault()
    if (!isTouch) return
    let touch = e.touches[0]
    let dx = touch.clientX - wrapCX
    let dy = touch.clientY - wrapCY
    let dist = Math.hypot(dx, dy)
    const r = wrapRect.width / 2
    if (dist > r) { dx = dx / dist * r; dy = dy / dist * r }
    joy.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`
    vx = dx / r * maxSpeed
    vy = dy / r * maxSpeed
})
joyWrap.addEventListener('touchend', () => {
    isTouch = false
    joy.style.transform = 'translate(-50%,-50%)'
    vx = 0; vy = 0
})

// 鼠标控制,方便电脑调试
let mouseDown = false
joyWrap.addEventListener('mousedown', e => {
    mouseDown = true
    wrapRect = joyWrap.getBoundingClientRect()
    wrapCX = wrapRect.left + wrapRect.width / 2
    wrapCY = wrapRect.top + wrapRect.height / 2
})
window.addEventListener('mousemove', e => {
    if (!mouseDown) return
    let dx = e.clientX - wrapCX
    let dy = e.clientY - wrapCY
    let dist = Math.hypot(dx, dy)
    const r = wrapRect.width / 2
    if (dist > r) { dx = dx / dist * r; dy = dy / dist * r }
    joy.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`
    vx = dx / r * maxSpeed
    vy = dy / r * maxSpeed
})
window.addEventListener('mouseup', () => {
    mouseDown = false
    joy.style.transform = 'translate(-50%,-50%)'
    vx = 0; vy = 0
})

// 莲花宝珠
const lotusSvg = `
<svg viewBox="0 0 26 26">
    <radialGradient id="lotus" cx="50%" cy="50%" r="50%">
        <stop offset="0%" stop-color="#fff7aa"/>
        <stop offset="60%" stop-color="#f5d76e"/>
        <stop offset="100%" stop-color="#d4af37"/>
    </radialGradient>
    <path fill="url(#lotus)" d="M13,2 C15,7 19,9 24,13 C19,17 15,19 13,24 C11,19 7,17 2,13 C7,9 11,7 13,2 Z"/>
    <circle cx="13" cy="13" r="3" fill="#fffbe0"/>
</svg>`

function makeStar(){
    for (let i = 0; i < totalStar; i++) {
        let s = document.createElement('div')
        s.className = 'star'
        s.innerHTML = lotusSvg
        let sx = 30 + Math.random() * (W - 60)
        let sy = 70 + Math.random() * (H - 140)
        s.style.left = sx + 'px'
        s.style.top = sy + 'px'
        box.appendChild(s)
        stars.push({ dom: s, x: sx, y: sy })
    }
}

function makeShadow(){
    let count = 2 + wave
    for (let i = 0; i < count; i++) {
        let sd = document.createElement('div')
        sd.className = 'shadow'
        let sx = 120 + Math.random() * (W - 180)
        let sy = 80 + Math.random() * (H - 160)
        sd.style.left = sx + 'px'
        sd.style.top = sy + 'px'
        box.appendChild(sd)
        shadows.push({
            dom: sd, x: sx, y: sy,
            dx: (Math.random() > 0.5 ? 1 : -1) * (1.5 + wave * 0.4),
            dy: (Math.random() > 0.5 ? 1 : -1) * (1.2 + wave * 0.3)
        })
    }
}

function makeSand(){
    for (let i = 0; i < 18; i++) {
        let p = document.createElement('div')
        p.className = 'sand'
        p.style.left = Math.random() * W + 'px'
        p.style.top = Math.random() * H + 'px'
        box.appendChild(p)
        particles.push({
            dom: p,
            x: parseFloat(p.style.left),
            y: parseFloat(p.style.top),
            vx: 1 + Math.random() * 2,
            vy: (Math.random() - 0.5) * 0.8
        })
    }
}

function sparkle(x, y){
    let sp = document.createElement('div')
    sp.className = 'sparkle show'
    sp.style.left = (x - 27) + 'px'
    sp.style.top = (y - 27) + 'px'
    box.appendChild(sp)
    setTimeout(() => sp.remove(), 600)
}

function hit(a, b, ar = 18, br = 16){
    let dx = a.x - b.x, dy = a.y - b.y
    return Math.sqrt(dx * dx + dy * dy) < ar + br
}

function nextWave(){
    if (wave >= maxWave) {
        winBox.style.display = 'flex'
        return
    }
    wave++
    totalStar += 1
    collected = 0
    scoreDom.innerText = 0
    totalDom.innerText = totalStar
    waveDom.innerText = wave
    stars.forEach(s => s.dom.remove())
    shadows.forEach(s => s.dom.remove())
    stars = []
    shadows = []
    makeStar()
    makeShadow()
}

function loop(){
    if (winBox.style.display === 'flex' || loseBox.style.display === 'flex') {
        requestAnimationFrame(loop)
        return
    }

    x += vx
    y += vy
    x = Math.max(0, Math.min(W - 40, x))
    y = Math.max(60, Math.min(H - 32, y))
    me.style.left = x + 'px'
    me.style.top = y + 'px'
    updateFacing()

    // 收集宝珠
    for (let i = stars.length - 1; i >= 0; i--) {
        if (hit({ x: x + 20, y: y + 16 }, stars[i], 18, 16)) {
            sparkle(stars[i].x + 13, stars[i].y + 13)
            stars[i].dom.remove()
            stars.splice(i, 1)
            collected++
            scoreDom.innerText = collected
            if (collected >= totalStar) {
                setTimeout(nextWave, 600)
            }
        }
    }

    // 风沙暗影
    shadows.forEach(s => {
        s.x += s.dx
        s.y += s.dy
        if (s.x < 0 || s.x > W - 40) s.dx *= -1
        if (s.y < 60 || s.y > H - 40) s.dy *= -1
        s.dom.style.left = s.x + 'px'
        s.dom.style.top = s.y + 'px'
        if (hit({ x: x + 20, y: y + 16 }, s, 18, 20)) {
            loseBox.style.display = 'flex'
        }
    })

    // 风沙颗粒
    particles.forEach(p => {
        p.x += p.vx
        p.y += p.vy
        if (p.x > W) p.x = -4
        if (p.y < 0) p.y = H
        if (p.y > H) p.y = 0
        p.dom.style.left = p.x + 'px'
        p.dom.style.top = p.y + 'px'
    })

    requestAnimationFrame(loop)
}

function restart(){
    box.querySelectorAll('.star,.shadow,.sparkle,.sand').forEach(el => el.remove())
    winBox.style.display = 'none'
    loseBox.style.display = 'none'
    stars = []
    shadows = []
    particles = []
    collected = 0
    totalStar = 6
    wave = 1
    scoreDom.innerText = 0
    totalDom.innerText = totalStar
    waveDom.innerText = wave
    x = 40; y = 80
    me.style.left = x + 'px'
    me.style.top = y + 'px'
    makeStar()
    makeShadow()
    makeSand()
}
restart()
loop()
</script>

</body>
</html>

Game Source: 敦煌寻星 · 壁画逃影

Creator: GoldenPilot43

Libraries: none

Complexity: complex (583 lines, 15.3 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-goldenpilot43" to link back to the original. Then publish at arcadelab.ai/publish.