摇一摇赛龙舟·端午争渡
by DriftMeteor50671 lines25.0 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<!-- viewport 设置,禁止用户缩放,保证在大屏上正常显示 -->
<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;
user-select: none; /* 避免选中文字,提升操作手感 */
}
body {
width: 100%;
min-height: 100vh;
background: linear-gradient(145deg, #0a3e2c 0%, #0a2f1f 100%);
font-family: 'Segoe UI', 'PingFang SC', Roboto, 'Noto Sans CJK SC', 'Microsoft YaHei', sans-serif;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
/* 主游戏卡片 */
.game-container {
width: 100%;
max-width: 800px;
background: rgba(255,245,220,0.12);
backdrop-filter: blur(3px);
border-radius: 72px 72px 48px 48px;
box-shadow: 0 25px 45px rgba(0,0,0,0.4), inset 0 1px 2px rgba(255,255,200,0.2);
padding: 18px 20px 30px;
transition: all 0.2s;
}
/* 标题区 + 端午装饰 */
.title-area {
text-align: center;
margin-bottom: 15px;
position: relative;
}
h1 {
font-size: clamp(32px, 8vw, 56px);
background: linear-gradient(135deg, #FFE6A7, #FFB347, #FF8C42);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
text-shadow: 0 2px 5px rgba(0,0,0,0.3);
letter-spacing: 0.08em;
}
.sub {
font-size: 14px;
color: #FFDEAD;
background: rgba(0,0,0,0.4);
display: inline-block;
padding: 4px 16px;
border-radius: 60px;
margin-top: 6px;
backdrop-filter: blur(4px);
}
/* 龙舟竞渡主视觉 */
.race-track {
background: #1A5940;
border-radius: 48px;
padding: 20px 12px 12px;
margin: 10px 0 20px;
box-shadow: inset 0 0 0 2px rgba(255,220,150,0.6), inset 0 0 0 6px #2F6B4A, 0 12px 20px rgba(0,0,0,0.3);
}
.river {
position: relative;
background: linear-gradient(180deg, #3B9C6E, #1C6B3F);
border-radius: 60px;
min-height: 220px;
overflow: hidden;
box-shadow: inset 0 5px 12px rgba(0,0,0,0.3), 0 1px 4px rgba(255,255,200,0.3);
}
/* 波浪装饰 */
.wave {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 48px;
background: repeating-linear-gradient( transparent 0px, transparent 28px, rgba(220,240,255,0.4) 28px, rgba(200,225,250,0.5) 34px);
pointer-events: none;
z-index: 1;
}
/* 龙舟本体 */
.dragon-boat {
position: absolute;
bottom: 20px;
left: 5%;
width: 95px;
transition: left 0.08s linear;
filter: drop-shadow(2px 6px 8px rgba(0,0,0,0.5));
z-index: 10;
pointer-events: none;
}
.dragon-boat img {
width: 100%;
display: block;
}
/* 排名面板 */
.ranking-panel {
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(12px);
border-radius: 32px;
padding: 14px 18px;
margin: 12px 0 0;
border: 1px solid #FFC285;
color: #FEF2DF;
}
.panel-title {
display: flex;
justify-content: space-between;
border-bottom: 1px solid #E6B87E;
padding-bottom: 8px;
margin-bottom: 10px;
font-weight: bold;
font-size: 18px;
}
.rank-list {
max-height: 188px;
overflow-y: auto;
}
.rank-item {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255,245,200,0.1);
margin: 6px 0;
padding: 6px 12px;
border-radius: 60px;
font-size: 15px;
}
.rank-number {
width: 32px;
font-weight: bold;
color: #FFC489;
}
.player-name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 12px;
}
.score {
background: #FFA559;
padding: 2px 12px;
border-radius: 30px;
font-weight: bold;
color: #2C2B2B;
}
/* 按钮组 */
.action-buttons {
display: flex;
justify-content: center;
gap: 18px;
margin: 22px 0 12px;
flex-wrap: wrap;
}
.btn {
border: none;
background: #FFCF9A;
color: #422b1a;
font-weight: bold;
padding: 12px 26px;
border-radius: 48px;
font-size: 17px;
cursor: pointer;
transition: 0.15s;
box-shadow: 0 4px 6px rgba(0,0,0,0.2);
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: #FFB347;
color: #2F2E2E;
box-shadow: 0 5px 0 #B4621A;
transform: translateY(-2px);
}
.btn-primary:active {
transform: translateY(3px);
box-shadow: 0 1px 0 #B4621A;
}
.btn-secondary {
background: #4F9A6F;
color: white;
box-shadow: none;
}
.shake-hint {
text-align: center;
color: #FFE1A0;
font-size: 14px;
background: rgba(0,0,0,0.5);
border-radius: 40px;
width: fit-content;
margin: 12px auto 0;
padding: 6px 24px;
display: flex;
align-items: center;
gap: 8px;
}
/* 参赛者输入浮层 */
.name-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.85);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
backdrop-filter: blur(8px);
}
.name-card {
background: #fef3db;
max-width: 300px;
width: 80%;
border-radius: 56px;
padding: 28px 18px;
text-align: center;
box-shadow: 0 20px 35px black;
}
.name-card input {
width: 80%;
padding: 12px 16px;
font-size: 18px;
margin: 20px 0;
border: 2px solid #CA8E4A;
border-radius: 56px;
text-align: center;
}
.confirm-btn {
background: #E07C2C;
color: white;
border: none;
padding: 10px 30px;
border-radius: 40px;
font-weight: bold;
}
.permission-tip {
background: #f9e2b7;
border-left: 7px solid #ff9f4a;
color: #32261a;
padding: 8px 12px;
border-radius: 20px;
margin: 10px 0;
font-size: 14px;
}
footer {
text-align: center;
font-size: 11px;
color: #B7C9B3;
margin-top: 18px;
}
/* 小屏适配 */
@media (max-width: 550px) {
.game-container { padding: 12px; }
.dragon-boat { width: 70px; }
.rank-item { font-size: 12px; }
.btn { padding: 8px 18px; font-size: 14px;}
}
</style>
</head>
<body>
<div class="game-container" id="gameApp">
<div class="title-area">
<h1>🐉 摇一摇·龙舟竞渡 🥁</h1>
<div class="sub">端午安康 · 赛出风采</div>
</div>
<div class="race-track">
<div class="river" id="riverTrack">
<div class="wave"></div>
<div class="dragon-boat" id="dragonBoat">
<!-- 使用 Emoji + SVG 风格龙舟,无需额外图片资源,零物料 -->
<div style="font-size: 68px; line-height: 1; text-align: center; filter: drop-shadow(2px 6px 6px #00000055);">🚣♂️🐉</div>
</div>
</div>
</div>
<!-- 排名面板 -->
<div class="ranking-panel">
<div class="panel-title">
<span>🏆 赛况龙舟榜 🏆</span>
<span>🏁 目标: 500桨 🏁</span>
</div>
<div class="rank-list" id="rankList">
<div style="text-align:center; padding: 10px;">等待选手上场...</div>
</div>
</div>
<div class="action-buttons">
<button class="btn btn-primary" id="joinRaceBtn">✨ 我要参赛 ✨</button>
<button class="btn btn-secondary" id="resetRaceBtn">🔄 重置擂台</button>
</div>
<div class="shake-hint" id="shakeHint">
📱 疯狂摇动手机 → 龙舟向前冲!🎋
</div>
<footer>🎋 摇得越快,龙舟越快,先达500桨者夺冠 🎋</footer>
</div>
<script>
// 简易状态管理,纯前端排名(本次活动数据不依赖后端, 但满足300人氛围)
// 为了「各团队自行组织 + 大屏展示」,所有参赛玩家存储在当前浏览器localStorage,但单一会场大屏下完全够用
// 如果是大屏投射(一台电脑/ipad投屏, 观众用自己的手机扫码访问同一个网址摇动,是独立排名)
// 注意: 摇一摇仅手机生效, 大屏主持人可手动重置或展示最终英雄榜
// ---------- 游戏全局参数 -------------
const GOAL_SCORE = 500; // 目标桨数
const SHAKE_COUNT_PER_THRESHOLD = 1; // 每次有效“摇动”增加1桨 (真实竞速感)
let raceActive = true; // 比赛是否进行中(有人达到500后暂停摇一摇增量)
let currentPlayerId = null; // 当前参与者ID
let currentPlayerName = null;
// 排行榜数组 { id, name, score, finishTime }
let leaderboard = [];
// 摇一摇防连击 & 节流相关变量
let lastShakeTime = 0;
let isProcessingShake = false;
let lastAcceleration = { x: 0, y: 0, z: 0 };
let shakeCooldown = 300; // 毫秒内多次摇动只算一次,保证公平性
let motionListenerActive = false; // 监听器是否已绑定
let devicemotionHandler = null;
// DOM 元素
const dragonBoatEl = document.getElementById('dragonBoat');
const rankListDiv = document.getElementById('rankList');
const joinBtn = document.getElementById('joinRaceBtn');
const resetBtn = document.getElementById('resetRaceBtn');
const shakeHintDiv = document.getElementById('shakeHint');
// 获取存储的排名数据 (本地存储)
function loadLeaderboard() {
const stored = localStorage.getItem('dragonBoat_rank');
if(stored) {
try{
leaderboard = JSON.parse(stored);
if(!Array.isArray(leaderboard)) leaderboard = [];
// 清理未完成的玩家(仅保留分数<GOAL_SCORE的排名中的非冠军,如果某玩家原本分数>=GOAL且未在当前活跃时,保持但视作已经完赛)
leaderboard = leaderboard.filter(p => p && typeof p.score === 'number');
leaderboard.sort((a,b) => {
if(a.score >= GOAL_SCORE && b.score >= GOAL_SCORE) return (a.finishTime||Infinity) - (b.finishTime||Infinity);
if(a.score >= GOAL_SCORE) return -1;
if(b.score >= GOAL_SCORE) return 1;
return b.score - a.score;
});
} catch(e){ leaderboard = []; }
} else {
leaderboard = [];
}
renderRanking();
}
function saveLeaderboard() {
localStorage.setItem('dragonBoat_rank', JSON.stringify(leaderboard));
renderRanking();
}
function renderRanking() {
if(!rankListDiv) return;
const activePlayers = leaderboard.filter(p => p.score < GOAL_SCORE);
const finishedPlayers = leaderboard.filter(p => p.score >= GOAL_SCORE).sort((a,b)=> (a.finishTime||0) - (b.finishTime||0));
const combined = [...finishedPlayers, ...activePlayers.sort((a,b)=>b.score - a.score)];
if(combined.length === 0) {
rankListDiv.innerHTML = '<div style="text-align:center; padding: 12px;">🏮 摇动手机,抢占龙舟榜首 🏮</div>';
return;
}
let html = '';
combined.forEach((player, idx) => {
const isChampion = (player.score >= GOAL_SCORE);
let statusIcon = isChampion ? '🏆 ' : (player.score > 0 ? '🐉 ' : '⛵ ');
let scoreDisplay = `${player.score} / ${GOAL_SCORE}`;
if(isChampion) scoreDisplay = `🏁 夺冠 ${player.score}`;
html += `<div class="rank-item">
<span class="rank-number">${idx+1}</span>
<span class="player-name">${statusIcon} ${escapeHtml(player.name)}</span>
<span class="score">${scoreDisplay}</span>
</div>`;
});
rankListDiv.innerHTML = html;
// 如果游戏已结束且所有玩家都达到终点,自动锁定摇一摇增量直到重置
if(activePlayers.length === 0 && combined.length>0 && combined.every(p=>p.score>=GOAL_SCORE)) {
raceActive = false;
shakeHintDiv.innerHTML = '🏮 竞渡终章!点击「重置擂台」再启征程 🏮';
} else {
if(!raceActive && combined.some(p=>p.score < GOAL_SCORE)) raceActive = true;
if(raceActive) shakeHintDiv.innerHTML = '📱 疯狂摇动手机 → 龙舟向前冲!🎋';
}
}
// 简单防XSS
function escapeHtml(str) {
if(!str) return '无名桨手';
return str.replace(/[&<>]/g, function(m) {
if(m === '&') return '&';
if(m === '<') return '<';
if(m === '>') return '>';
return m;
}).substring(0, 16);
}
// 增加桨数
function addScore(playerId, increment = 1) {
if(!raceActive) return false;
const player = leaderboard.find(p => p.id === playerId);
if(!player) return false;
if(player.score >= GOAL_SCORE) return false; // 已完赛不再加分
let newScore = Math.min(player.score + increment, GOAL_SCORE);
player.score = newScore;
if(newScore >= GOAL_SCORE && !player.finishTime) {
player.finishTime = Date.now();
raceActive = false; // 有人夺冠立即冻结摇一摇,不再接受新桨
shakeHintDiv.innerHTML = `🎉 恭喜「${escapeHtml(player.name)}」率先破浪夺魁!🎉 重置后再次争渡~ 🎉`;
// 通知大屏效果
setTimeout(() => {
if(leaderboard.some(p=>p.score < GOAL_SCORE)) raceActive = true;
else raceActive = false;
renderRanking();
},100);
}
saveLeaderboard();
// 更新龙舟动画: 根据当前玩家的进度展示偏移比例
if(currentPlayerId === playerId) {
updateBoatProgress(player.score);
}
return true;
}
// 更新UI龙舟位置 (按分数比例,不超过河道宽度的85%)
function updateBoatProgress(score) {
if(!dragonBoatEl) return;
const river = document.getElementById('riverTrack');
if(!river) return;
const riverWidth = river.clientWidth;
const maxLeftPercent = Math.min(0.85, (score / GOAL_SCORE) * 0.92);
const leftPercent = Math.min(0.85, Math.max(0.05, maxLeftPercent));
dragonBoatEl.style.left = `calc(${leftPercent * 100}% - 25px)`;
if(score >= GOAL_SCORE) {
dragonBoatEl.style.filter = "drop-shadow(0 0 6px gold)";
} else {
dragonBoatEl.style.filter = "drop-shadow(2px 6px 8px rgba(0,0,0,0.5))";
}
}
// 创建玩家或者切换玩家
function registerAndStart(playerName) {
if(!playerName || playerName.trim() === '') playerName = "粽情桨手";
playerName = playerName.trim().substring(0, 16);
// 查找是否已存在同名玩家(这里简单起见,允许同名不同ID,但榜单展示好)
let existing = leaderboard.find(p => p.name === playerName && p.score < GOAL_SCORE);
if(existing) {
currentPlayerId = existing.id;
currentPlayerName = existing.name;
updateBoatProgress(existing.score);
shakeHintDiv.innerHTML = `🐉 继续为「${escapeHtml(currentPlayerName)}」摇桨! 当前${existing.score}/${GOAL_SCORE}`;
if(existing.score >= GOAL_SCORE) {
shakeHintDiv.innerHTML = `🏁 ${escapeHtml(playerName)} 已斩获冠军,无法划桨,请重置擂台 🏁`;
raceActive = false;
} else {
raceActive = true;
}
renderRanking();
return;
}
// 新玩家
const newId = 'pid_' + Date.now() + '_' + Math.floor(Math.random()*10000);
const newPlayer = {
id: newId,
name: playerName,
score: 0,
finishTime: null
};
leaderboard.push(newPlayer);
currentPlayerId = newId;
currentPlayerName = playerName;
raceActive = true;
updateBoatProgress(0);
saveLeaderboard();
shakeHintDiv.innerHTML = `🌟 ${escapeHtml(playerName)} 登船!快摇动手机冲线 🌟`;
}
// 重置所有数据(保留荣耀? 为了趣味完全重置。)
function resetFullGame() {
if(confirm('🏮 重置龙舟赛会清空所有玩家记录,是否继续? 🏮')) {
leaderboard = [];
currentPlayerId = null;
currentPlayerName = null;
raceActive = true;
localStorage.removeItem('dragonBoat_rank');
updateBoatProgress(0);
renderRanking();
shakeHintDiv.innerHTML = '📱 疯狂摇动手机 → 龙舟向前冲!🎋';
if(devicemotionHandler) {
// 保留监听器,避免重新绑定
}
// 若当前没玩家,提示加入
if(!currentPlayerName) {
showNamePrompt();
}
}
}
// 显示输入昵称层
function showNamePrompt() {
const overlay = document.createElement('div');
overlay.className = 'name-overlay';
overlay.id = 'nameOverlay';
overlay.innerHTML = `
<div class="name-card">
<h3 style="color:#7A4316;">🐉 端午竞渡 🐉</h3>
<p style="font-size:14px;">请留下你的大名,摇桨赢龙舟</p>
<input type="text" id="playerNameInput" placeholder="如: 屈子桨手" maxlength="12">
<br>
<button class="confirm-btn" id="confirmNameBtn">启 程 争 渡</button>
</div>
`;
document.body.appendChild(overlay);
const input = document.getElementById('playerNameInput');
const confirmBtn = document.getElementById('confirmNameBtn');
const closeOverlay = (name) => {
if(name && name.trim()) registerAndStart(name.trim());
else registerAndStart('龙舟勇士');
overlay.remove();
requestMotionPermissionIfNeeded(); // 绑定摇一摇权限
};
confirmBtn.onclick = () => { closeOverlay(input.value); };
input.addEventListener('keypress', (e) => { if(e.key === 'Enter') closeOverlay(input.value); });
if(!window.DeviceMotionEvent) {
// fallback
}
}
// 摇一摇核心函数 (基于 DeviceMotionEvent)
function initShakeDetection() {
if(devicemotionHandler) {
window.removeEventListener('devicemotion', devicemotionHandler);
}
let lastTimestamp = 0;
const SHAKE_SENSITIVITY = 23; // 摇动强度阈值
devicemotionHandler = function(event) {
if(!raceActive) return;
if(!currentPlayerId) {
// 没选手时提醒
if(Date.now() % 4000 < 500) shakeHintDiv.innerHTML = '🥁 先点击「我要参赛」再摇龙舟 🥁';
return;
}
const acc = event.accelerationIncludingGravity;
if(!acc) return;
let x = acc.x || 0;
let y = acc.y || 0;
let z = acc.z || 0;
const nowTime = Date.now();
if(nowTime - lastTimestamp < 120) return; //节流提升性能
lastTimestamp = nowTime;
// 计算加速度变化强度(差值模长)
let diff = Math.sqrt(
Math.pow(x - lastAcceleration.x, 2) +
Math.pow(y - lastAcceleration.y, 2) +
Math.pow(z - lastAcceleration.z, 2)
);
// 更新上次加速度
lastAcceleration = { x, y, z };
if(diff > SHAKE_SENSITIVITY && nowTime - lastShakeTime > shakeCooldown) {
lastShakeTime = nowTime;
if(currentPlayerId && raceActive) {
addScore(currentPlayerId, SHAKE_COUNT_PER_THRESHOLD);
// 增加摇动视觉反馈
dragonBoatEl.style.transform = 'scale(1.04)';
setTimeout(()=>{ if(dragonBoatEl) dragonBoatEl.style.transform = ''; }, 100);
if(currentPlayerId) {
const p = leaderboard.find(i=>i.id===currentPlayerId);
if(p) shakeHintDiv.innerHTML = `🚣♂️ ${escapeHtml(p.name)} 摇桨 +1 ! 总桨数: ${p.score}/${GOAL_SCORE}`;
}
}
}
};
window.addEventListener('devicemotion', devicemotionHandler);
motionListenerActive = true;
}
// iOS 13+ 权限请求 (必须由用户手势触发)
function requestMotionPermissionIfNeeded() {
if (typeof DeviceMotionEvent !== 'undefined' && typeof DeviceMotionEvent.requestPermission === 'function') {
// iOS 设备需要权限
DeviceMotionEvent.requestPermission()
.then(permissionState => {
if (permissionState === 'granted') {
initShakeDetection();
} else {
shakeHintDiv.innerHTML = '⚠️ 未授权运动传感器,无法摇龙舟 ⚠️';
}
})
.catch(err => {
shakeHintDiv.innerHTML = '⚠️ 请点击页面允许传感器权限 ⚠️';
});
} else {
// Android 或者其他直接初始化
initShakeDetection();
}
}
// 结合参赛按钮触发权限请求
function startWithPermissionAndJoin() {
if (!currentPlayerName) {
showNamePrompt();
} else {
requestMotionPermissionIfNeeded();
}
}
// 绑定界面按钮事件
joinBtn.onclick = () => {
showNamePrompt();
};
resetBtn.onclick = () => {
resetFullGame();
};
// 初始化加载排名并显示注册
loadLeaderboard();
// 如果已存在玩家则自动设定当前显示龙舟位置
if(leaderboard.length > 0) {
// 可以选取目前榜上第一未完成者作为默认显示
let unfinished = leaderboard.find(p => p.score < GOAL_SCORE);
if(unfinished) {
currentPlayerId = unfinished.id;
currentPlayerName = unfinished.name;
updateBoatProgress(unfinished.score);
raceActive = true;
} else {
if(leaderboard[0]) updateBoatProgress(leaderboard[0].score);
}
} else {
updateBoatProgress(0);
}
// 尺寸变化防抖刷新龙舟位置
window.addEventListener('resize', () => {
if(currentPlayerId){
const p = leaderboard.find(i=>i.id===currentPlayerId);
if(p) updateBoatProgress(p.score);
} else updateBoatProgress(0);
});
// 自动请求权限(如果以前已经授权但未触发)
setTimeout(() => {
if(currentPlayerName && !motionListenerActive){
if (typeof DeviceMotionEvent !== 'undefined' && !DeviceMotionEvent.requestPermission) {
initShakeDetection();
}
}
}, 800);
// 补充说明: 该游戏支持大屏 + 手机用户一同竞技。所有数据基于本地存储,会议主持人可投屏展示排位
</script>
</body>
</html>
Game Source: 摇一摇赛龙舟·端午争渡
Creator: DriftMeteor50
Libraries: none
Complexity: complex (671 lines, 25.0 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-driftmeteor50" to link back to the original. Then publish at arcadelab.ai/publish.