敦煌寻星 · 壁画逃影
by GoldenPilot43583 lines15.3 KB
<!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.