console
<!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>Phigros Web</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
body { overflow: hidden; background: #050510; touch-action: none; font-family: sans-serif; }
#game {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: radial-gradient(circle at center, #12122b 0%, #050510 100%);
z-index: 1;
}
#score-board {
position: absolute; top: 20px; left: 20px; color: #fff; font-size: 18px;
font-weight: bold; letter-spacing: 2px; z-index: 10;
}
#combo-board {
position: absolute; top: 30%; left: 50%; transform: translateX(-50%);
color: #fff; font-size: 50px; font-weight: 900; font-style: italic;
text-shadow: 0 0 20px #0ff; z-index: 6; pointer-events: none;
}
#judge-line {
position: absolute; bottom: 20vh; left: 0; width: 100%; height: 4px;
background: #fff; box-shadow: 0 0 12px #0ff, 0 0 24px #0ff;
z-index: 5; pointer-events: none;
}
.note {
position: absolute; width: 80px; height: 14px; margin-left: -40px; margin-top: -7px;
background: linear-gradient(90deg, #00e5ff, #007bff);
border-radius: 7px; box-shadow: 0 0 15px rgba(0, 229, 255, 0.8);
pointer-events: none; z-index: 2;
}
.note::after {
content: ''; position: absolute; top: 50%; left: 10%; right: 10%; height: 2px;
background: #fff; transform: translateY(-50%);
}
.hit-effect {
position: absolute; width: 120px; height: 12px; margin-left: -60px; margin-top: -6px;
background: #fff; box-shadow: 0 0 20px #fff, 0 0 40px #0ff;
pointer-events: none; z-index: 20; animation: hitAnim 0.2s ease-out forwards;
}
@keyframes hitAnim {
0% { transform: scaleX(0.5) scaleY(2); opacity: 1; }
100% { transform: scaleX(1.5) scaleY(0.1); opacity: 0; }
}
.judge-text {
position: absolute; color: #fff; font-weight: 900; font-size: 18px; font-style: italic;
pointer-events: none; z-index: 25; transform: translate(-50%, -50%);
animation: textFade 0.4s ease-out forwards;
}
.judge-text.perfect { color: #ffd700; text-shadow: 0 0 10px #ffd700; }
.judge-text.good { color: #00e5ff; text-shadow: 0 0 10px #00e5ff; }
@keyframes textFade {
0% { opacity: 1; transform: translate(-50%, -50%) scale(0.8); }
50% { opacity: 1; transform: translate(-50%, -60%) scale(1.1); }
100% { opacity: 0; transform: translate(-50%, -80%) scale(1); }
}
#start-screen {
position: absolute; inset: 0; background: rgba(0,0,0,0.85); z-index: 100;
display: flex; flex-direction: column; align-items: center; justify-content: center;
}
#start-screen h2 { color: #fff; letter-spacing: 4px; margin-bottom: 20px; }
#start-btn {
padding: 15px 50px; font-size: 20px; border: 2px solid #0ff; border-radius: 30px;
background: transparent; color: #0ff; font-weight: bold; cursor: pointer;
box-shadow: 0 0 15px #0ff, inset 0 0 15px #0ff;
}
#over {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.85); color: #fff;
display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 30;
}
#over h2 { font-size: 32px; font-weight: 300; letter-spacing: 4px; margin-bottom: 15px; }
#over p { font-size: 16px; color: #aaa; margin-bottom: 30px; }
#over span { color: #fff; font-size: 36px; font-weight: bold; display: block; margin-top: 5px; }
#over button {
padding: 10px 30px; font-size: 16px; border-radius: 5px; border: 1px solid #fff;
background: transparent; color: #fff; font-weight: bold; cursor: pointer;
}
</style>
</head>
<body>
<div id="game">
<div id="score-board">SCORE: <span id="score-val">0</span></div>
<div id="judge-line"></div>
<div id="combo-board" class="hide">0</div>
<div id="start-screen">
<h2>PHIGROS WEB</h2>
<button id="start-btn">START</button>
</div>
</div>
<div id="over">
<h2>GAME OVER</h2>
<p>最终得分 <span id="final-score">0</span></p>
<button onclick="location.reload()">RESTART</button>
</div>
<script>
const game = document.getElementById('game');
const scoreVal = document.getElementById('score-val');
const comboBoard = document.getElementById('combo-board');
const overScreen = document.getElementById('over');
const finalScoreEl = document.getElementById('final-score');
const judgeLineEl = document.getElementById('judge-line');
const startScreen = document.getElementById('start-screen');
const startBtn = document.getElementById('start-btn');
let score = 0, combo = 0, timeLeft = 30, isOver = false;
let notes = [];
let judgeY = 0;
let speed = 7;
let gameRunning = false;
startBtn.addEventListener('click', () => {
startScreen.style.display = 'none';
gameRunning = true;
let rect = judgeLineEl.getBoundingClientRect();
judgeY = rect.top > 100 ? rect.top : (window.innerHeight || 800) * 0.8;
spawnNote();
gameLoop();
setInterval(() => {
if (isOver) return;
timeLeft--;
if (timeLeft <= 0) gameOver();
}, 1000);
});
function spawnNote() {
if (isOver || !gameRunning) return;
const note = document.createElement('div');
note.className = 'note';
const x = Math.random() * (window.innerWidth - 120) + 60;
note.style.left = x + 'px';
note.style.top = '-30px';
notes.push({ el: note, x: x, y: -30, isHit: false });
game.appendChild(note);
let rate = Math.max(350, 650 - (30 - timeLeft) * 8);
setTimeout(spawnNote, Math.random() * 150 + rate);
}
function gameLoop() {
if (isOver || !gameRunning) return;
let rect = judgeLineEl.getBoundingClientRect();
if(rect.top > 0) judgeY = rect.top;
for (let i = notes.length - 1; i >= 0; i--) {
let n = notes[i];
if (n.isHit) continue;
n.y += speed;
n.el.style.top = n.y + 'px';
if (n.y > judgeY + 100) {
n.el.remove();
notes.splice(i, 1);
combo = 0;
updateUI();
}
}
requestAnimationFrame(gameLoop);
}
game.addEventListener('pointerdown', (e) => {
if (isOver || !gameRunning) return;
const clickX = e.clientX;
let closestDist = Infinity;
let closestIndex = -1;
for (let i = 0; i < notes.length; i++) {
let n = notes[i];
if (n.isHit) continue;
if (Math.abs(n.y - judgeY) < 80 && Math.abs(n.x - clickX) < 60) {
let dist = Math.abs(n.y - judgeY);
if (dist < closestDist) {
closestDist = dist;
closestIndex = i;
}
}
}
if (closestIndex !== -1) {
let n = notes[closestIndex];
n.isHit = true;
n.el.remove();
notes.splice(closestIndex, 1);
let judgeType = closestDist < 40 ? 'PERFECT' : 'GOOD';
combo++;
score += (judgeType === 'PERFECT' ? 100 : 50) + combo * 2;
updateUI();
showHitEffect(n.x, judgeY);
showJudgeText(n.x, judgeY, judgeType);
}
});
function showHitEffect(x, y) {
const effect = document.createElement('div');
effect.className = 'hit-effect';
effect.style.left = x + 'px';
effect.style.top = y + 'px';
game.appendChild(effect);
setTimeout(() => effect.remove(), 200);
}
function showJudgeText(x, y, text) {
const t = document.createElement('div');
t.className = `judge-text ${text.toLowerCase()}`;
t.innerText = text;
t.style.left = x + 'px';
t.style.top = (y - 40) + 'px';
game.appendChild(t);
setTimeout(() => t.remove(), 400);
}
function updateUI() {
scoreVal.innerText = score;
if (combo > 0) {
comboBoard.classList.remove('hide');
comboBoard.innerText = combo;
} else {
comboBoard.classList.add('hide');
}
}
function gameOver() {
isOver = true;
finalScoreEl.innerText = score;
overScreen.style.display = 'flex';
}
</script>
</body>
</html>