垃圾小卫士
by MysticLion442113 lines93.1 KB
<!DOCTYPE html>
<html lang="zh">
<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>
* {
box-sizing: border-box;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
body {
margin: 0;
min-height: 100vh;
background: #2c3e50;
font-family: 'Comic Sans MS', 'Chalkboard SE', cursive, sans-serif;
display: flex;
justify-content: center;
align-items: center;
touch-action: none;
padding: 4px;
}
#game-container {
width: 100%;
max-width: 500px;
height: 100vh;
max-height: 820px;
background: #ecf0f1;
border-radius: 32px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
padding: 8px;
}
/* ---------- 选择界面 ---------- */
#select-screen {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 14px;
text-align: center;
background: #ecf0f1;
border-radius: 28px;
padding: 20px 16px;
}
#select-screen h1 {
font-size: 2rem;
color: #2c3e50;
margin: 0;
line-height: 1.2;
}
#select-screen .subtitle {
font-size: 1.1rem;
color: #7f8c8d;
margin: -4px 0 4px 0;
}
#select-screen p {
font-size: 1.1rem;
color: #34495e;
margin: 0;
}
.select-bins {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
width: 100%;
max-width: 280px;
}
.select-bin {
background: white;
border-radius: 30px;
padding: 18px 0;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4px 0 rgba(0,0,0,0.15);
border: 4px solid #bdc3c7;
transition: transform 0.1s;
touch-action: manipulation;
cursor: pointer;
min-height: 88px;
justify-content: center;
}
.select-bin:active {
transform: scale(0.92);
}
.select-bin .bin-emoji {
font-size: 3rem;
line-height: 1;
}
.select-bin .bin-label {
font-size: 1.1rem;
font-weight: bold;
margin-top: 4px;
color: #2c3e50;
}
.select-bin.bin-recyclable {
border-color: #3498db;
background: #d6eaf8;
}
.select-bin.bin-hazardous {
border-color: #e74c3c;
background: #fadbd8;
}
.select-bin.bin-kitchen {
border-color: #27ae60;
background: #d5f5e3;
}
.select-bin.bin-other {
border-color: #95a5a6;
background: #ebedef;
}
.select-hint {
font-size: 0.9rem;
color: #7f8c8d;
background: rgba(255, 255, 255, 0.6);
padding: 6px 18px;
border-radius: 20px;
}
.high-score-display {
font-size: 1rem;
color: #f39c12;
font-weight: bold;
background: rgba(255, 255, 255, 0.7);
padding: 6px 20px;
border-radius: 22px;
border: 2px solid #f39c12;
}
/* ---------- 游戏界面 ---------- */
#game-screen {
display: none;
flex-direction: column;
height: 100%;
background: #ecf0f1;
border-radius: 28px;
overflow: hidden;
gap: 4px;
}
/* 顶部状态栏 */
#top-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 12px;
background: rgba(189, 195, 199, 0.6);
border-radius: 28px;
flex-shrink: 0;
height: 52px;
backdrop-filter: blur(4px);
z-index: 5;
gap: 6px;
flex-wrap: nowrap;
}
#timer-display {
font-size: 1.4rem;
font-weight: bold;
color: #2c3e50;
background: white;
padding: 4px 16px;
border-radius: 24px;
border: 2px solid #e74c3c;
white-space: nowrap;
min-width: 60px;
text-align: center;
}
#timer-display.warning {
color: #e74c3c;
animation: pulse 0.6s ease-in-out infinite alternate;
}
@keyframes pulse {
from { transform: scale(1); }
to { transform: scale(1.08); }
}
#score-display {
font-size: 1.5rem;
background: white;
padding: 4px 20px;
border-radius: 24px;
font-weight: bold;
border: 2px solid #2c3e50;
white-space: nowrap;
min-width: 70px;
text-align: center;
}
#score-display.negative {
border-color: #e74c3c;
color: #e74c3c;
}
#category-display {
font-size: 0.9rem;
background: #2c3e50;
color: white;
padding: 4px 14px;
border-radius: 20px;
white-space: nowrap;
}
/* ---------- 地图区域 ---------- */
#map-container {
flex: 1;
position: relative;
border-radius: 20px;
overflow: hidden;
border: 4px solid #7f8c8d;
background: #7ec87e;
min-height: 160px;
background-image: linear-gradient(rgba(255, 255, 255, 0.12) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.12) 1px, transparent 1px);
background-size: 28px 28px;
}
.road-h {
position: absolute;
height: 16px;
background: #bdc3c7;
border-top: 2px solid #95a5a6;
border-bottom: 2px solid #95a5a6;
z-index: 1;
pointer-events: none;
}
.road-v {
position: absolute;
width: 16px;
background: #bdc3c7;
border-left: 2px solid #95a5a6;
border-right: 2px solid #95a5a6;
z-index: 1;
pointer-events: none;
}
.tree {
position: absolute;
font-size: 1.8rem;
z-index: 2;
pointer-events: none;
text-shadow: 0 3px 6px rgba(0,0,0,0.15);
}
.flower {
position: absolute;
font-size: 1.2rem;
z-index: 1;
pointer-events: none;
}
.house {
position: absolute;
font-size: 2.2rem;
z-index: 2;
pointer-events: none;
text-shadow: 0 3px 6px rgba(0,0,0,0.15);
}
/* ---------- 垃圾桶(玩家) ---------- */
#player-bin {
position: absolute;
width: 64px;
height: 76px;
z-index: 10;
pointer-events: none;
transition: left 0.08s linear, top 0.08s linear;
filter: drop-shadow(0 6px 12px rgba(0,0,0,0.25));
}
#player-bin .bin-body {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
transition: transform 0.15s;
}
/* 桶身 */
#player-bin .bin-body .bucket {
width: 56px;
height: 48px;
border-radius: 8px 8px 4px 4px;
border: 3px solid #2c3e50;
border-top: none;
position: relative;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
font-weight: bold;
color: white;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
transition: background 0.3s;
background: #4a90e2;
box-shadow: inset 0 -6px 0 rgba(0,0,0,0.15), inset 0 6px 8px rgba(255,255,255,0.2);
}
/* 桶身条纹装饰(不同分类显示不同图标) */
#player-bin .bin-body .bucket .stripe {
position: absolute;
font-size: 1.8rem;
opacity: 0.2;
bottom: 6px;
right: 6px;
pointer-events: none;
}
/* 正面分类标识(取代表情) */
#player-bin .bin-body .face {
position: absolute;
font-size: 2.6rem;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
line-height: 1;
text-shadow: 0 2px 8px rgba(0,0,0,0.2);
z-index: 3;
pointer-events: none;
transition: all 0.2s;
}
/* 盖子 - 圆顶造型 */
#player-bin .bin-body .lid {
width: 48px;
height: 16px;
border-radius: 50% 50% 0 0;
border: 3px solid #2c3e50;
border-bottom: none;
margin-bottom: -4px;
position: relative;
z-index: 2;
transition: background 0.3s;
background: #7f8c8d;
box-shadow: inset 0 4px 6px rgba(255,255,255,0.3);
}
/* 盖子提手(弧形) */
#player-bin .bin-body .lid::after {
content: '';
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
width: 18px;
height: 8px;
border: 3px solid #2c3e50;
border-bottom: none;
border-radius: 50% 50% 0 0;
background: transparent;
}
/* 轮子 */
#player-bin .bin-body .wheel {
position: absolute;
bottom: -6px;
width: 16px;
height: 16px;
background: #2c3e50;
border-radius: 50%;
border: 2px solid #1a252f;
box-shadow: inset 0 -3px 0 rgba(0,0,0,0.3), inset 0 3px 4px rgba(255,255,255,0.2);
}
#player-bin .bin-body .wheel.left { left: 2px; }
#player-bin .bin-body .wheel.right { right: 2px; }
#player-bin .bin-body .wheel::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 4px;
height: 4px;
background: #7f8c8d;
border-radius: 50%;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.3);
}
/* 嘴巴(吃垃圾时张开) */
#player-bin .mouth-icon {
position: absolute;
font-size: 2.8rem;
top: -14px;
left: 50%;
transform: translateX(-50%) scale(0);
opacity: 0;
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 12;
pointer-events: none;
}
#player-bin .mouth-icon.show {
opacity: 1;
transform: translateX(-50%) scale(1.4);
}
#player-bin .stars {
position: absolute;
font-size: 2rem;
top: -24px;
right: -10px;
opacity: 0;
pointer-events: none;
z-index: 13;
transition: all 0.5s ease;
}
#player-bin .stars.show {
opacity: 1;
transform: translateY(-30px) scale(1.6);
}
#player-bin.walking .bin-body {
animation: walkSway 0.4s ease-in-out infinite alternate;
}
@keyframes walkSway {
0% { transform: rotate(-5deg) scale(1); }
100% { transform: rotate(5deg) scale(1.02); }
}
#player-bin.eating .bin-body {
animation: eatOpen 0.5s ease;
}
@keyframes eatOpen {
0% { transform: scale(1) rotate(0); }
30% { transform: scale(1.2) rotate(-8deg); }
60% { transform: scale(0.95) rotate(8deg); }
100% { transform: scale(1) rotate(0); }
}
#player-bin.kicking .bin-body {
animation: kickAction 0.6s ease;
}
@keyframes kickAction {
0% { transform: scale(1) rotate(0); }
20% { transform: scale(1.1) rotate(-18deg) translateX(-8px); }
40% { transform: scale(0.9) rotate(14deg) translateX(12px); }
60% { transform: scale(1.05) rotate(-6deg) translateX(-4px); }
100% { transform: scale(1) rotate(0) translateX(0); }
}
/* ---------- 垃圾物品 ---------- */
.garbage-item {
position: absolute;
font-size: 2.6rem;
line-height: 1;
width: 54px;
height: 54px;
display: flex;
justify-content: center;
align-items: center;
background: rgba(255,255,255,0.85);
border-radius: 50%;
border: 4px solid #2c3e50;
box-shadow: 0 4px 0 #7f8c8d;
z-index: 5;
pointer-events: none;
transition: transform 0.2s, opacity 0.3s;
}
/* 特殊垃圾标记 */
.garbage-item.special {
border-color: #8e44ad;
background: #f4ecf7;
box-shadow: 0 4px 0 #6c3483, 0 0 20px rgba(142,68,173,0.3);
animation: specialGlow 1.2s ease-in-out infinite alternate;
}
@keyframes specialGlow {
0% { box-shadow: 0 4px 0 #6c3483, 0 0 15px rgba(142,68,173,0.2); }
100% { box-shadow: 0 4px 0 #6c3483, 0 0 35px rgba(142,68,173,0.5); }
}
.garbage-item .special-tag {
position: absolute;
bottom: -22px;
font-size: 0.55rem;
background: #8e44ad;
color: white;
padding: 1px 8px;
border-radius: 10px;
white-space: nowrap;
font-weight: bold;
}
/* 吸入动画 */
.garbage-item.sucked {
animation: suckIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
pointer-events: none;
z-index: 20;
}
@keyframes suckIn {
0% { transform: scale(1) rotate(0deg); opacity:1; }
60% { transform: scale(0.5) rotate(200deg) translateY(-30px); opacity:0.9; }
100% { transform: scale(0) rotate(360deg) translateY(-60px); opacity:0; }
}
/* 踢飞动画 */
.garbage-item.flying {
animation: flyAway 0.8s ease forwards;
pointer-events: none;
}
@keyframes flyAway {
0% { transform: translate(0,0) scale(1) rotate(0); opacity:1; }
30% { transform: translate(60px,-80px) scale(1.3) rotate(40deg); opacity:1; }
100% { transform: translate(120px,-160px) scale(0.3) rotate(120deg); opacity:0; }
}
/* 交互提示 */
#interact-hint {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.8);
color: #f1c40f;
padding: 6px 18px;
border-radius: 28px;
font-size: 0.9rem;
z-index: 20;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
white-space: nowrap;
border: 2px solid #f1c40f;
font-weight: bold;
}
#interact-hint.show { opacity: 1; }
#interact-hint.special-hint {
border-color: #8e44ad;
color: #d7bde2;
background: rgba(46,0,70,0.85);
}
/* 反馈气泡 */
#feedback {
position: absolute;
top: 18%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 1.8rem;
font-weight: bold;
text-shadow: 0 2px 12px rgba(0,0,0,0.4);
pointer-events: none;
z-index: 20;
opacity: 0;
transition: all 0.25s ease;
background: rgba(0,0,0,0.75);
padding: 8px 22px;
border-radius: 40px;
color: white;
white-space: nowrap;
}
#feedback.show { opacity:1; transform: translate(-50%, -70%) scale(1.1); }
/* ---------- 控制面板 ---------- */
#control-panel {
flex-shrink: 0;
display: flex;
gap: 8px;
padding: 6px 6px 4px 6px;
background: rgba(189,195,199,0.4);
border-radius: 28px;
backdrop-filter: blur(4px);
z-index: 5;
height: 140px;
min-height: 120px;
}
#joystick-area {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 4px;
min-width: 0;
}
#joystick-base {
position: relative;
width: 100%;
max-width: 130px;
aspect-ratio: 1/1;
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.3), rgba(0,0,0,0.08));
border-radius: 50%;
border: 5px solid rgba(44,62,80,0.3);
box-shadow: inset 0 -4px 8px rgba(0,0,0,0.15), 0 4px 12px rgba(0,0,0,0.1);
touch-action: none;
cursor: grab;
transition: border-color 0.2s;
}
#joystick-base.active {
border-color: rgba(52,152,219,0.6);
box-shadow: 0 0 30px rgba(52,152,219,0.25);
}
#joystick-thumb {
position: absolute;
width: 44%;
aspect-ratio: 1/1;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: radial-gradient(circle at 35% 35%, #ecf0f1, #bdc3c7);
border-radius: 50%;
border: 4px solid #2c3e50;
box-shadow: 0 4px 12px rgba(0,0,0,0.25), inset 0 -3px 6px rgba(0,0,0,0.1);
pointer-events: none;
transition: box-shadow 0.2s;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.2rem;
color: #2c3e50;
font-weight: bold;
}
#joystick-thumb::after {
content: '⬤';
font-size: 0.8rem;
color: rgba(44,62,80,0.3);
}
#joystick-base.active #joystick-thumb {
box-shadow: 0 4px 20px rgba(52,152,219,0.4);
}
.joystick-arrow {
position: absolute;
font-size: 0.9rem;
color: rgba(44,62,80,0.25);
pointer-events: none;
}
.joystick-arrow.up { top:6px; left:50%; transform:translateX(-50%); }
.joystick-arrow.down { bottom:6px; left:50%; transform:translateX(-50%); }
.joystick-arrow.left { left:6px; top:50%; transform:translateY(-50%); }
.joystick-arrow.right { right:6px; top:50%; transform:translateY(-50%); }
#knob-area {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
padding: 4px 6px 4px 0;
min-width: 0;
}
.knob-btn {
flex: 1;
border-radius: 40px;
border: none;
font-family: inherit;
font-weight: bold;
font-size: 1.4rem;
color: white;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
box-shadow: 0 6px 0 rgba(0,0,0,0.2), inset 0 -2px 8px rgba(0,0,0,0.05);
cursor: pointer;
touch-action: manipulation;
transition: all 0.08s;
padding: 0 8px;
position: relative;
min-height: 54px;
}
.knob-btn:active {
transform: translateY(4px);
box-shadow: 0 2px 0 rgba(0,0,0,0.2);
}
.knob-btn .knob-icon { font-size: 2.2rem; line-height:1; }
.knob-btn .knob-label { font-size: 1.4rem; letter-spacing:2px; font-weight:900; }
.knob-btn::before {
content: '';
position: absolute;
inset: 4px;
border-radius: 34px;
border: 2px solid rgba(255,255,255,0.25);
pointer-events: none;
}
.knob-btn.eat-btn {
background: linear-gradient(145deg, #2ecc71, #27ae60);
border: 3px solid #1e8449;
}
.knob-btn.eat-btn:active { background: linear-gradient(145deg, #27ae60, #1e8449); }
.knob-btn.spit-btn {
background: linear-gradient(145deg, #e67e22, #d35400);
border: 3px solid #a04000;
}
.knob-btn.spit-btn:active { background: linear-gradient(145deg, #d35400, #a04000); }
.knob-btn .knob-dots {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
border-radius: 40px;
overflow: hidden;
}
.knob-btn .knob-dots span {
position: absolute;
width: 4px;
height: 8px;
background: rgba(255,255,255,0.2);
border-radius: 3px;
}
#bottom-extra {
display: flex;
gap: 8px;
justify-content: center;
padding: 4px 0 2px 0;
flex-shrink: 0;
}
.extra-btn {
background: rgba(255,255,255,0.7);
border: 2px solid #7f8c8d;
border-radius: 24px;
padding: 6px 20px;
font-size: 0.95rem;
font-weight: bold;
color: #2c3e50;
cursor: pointer;
touch-action: manipulation;
font-family: inherit;
box-shadow: 0 3px 0 #bdc3c7;
transition: all 0.08s;
min-height: 40px;
}
.extra-btn:active {
transform: translateY(3px);
box-shadow: 0 0px 0 #bdc3c7;
}
.extra-btn.reset {
background: #f39c12;
color: white;
border-color: #d68910;
box-shadow: 0 3px 0 #d68910;
}
.extra-btn.back {
background: #3498db;
color: white;
border-color: #2471a3;
box-shadow: 0 3px 0 #2471a3;
}
/* ---------- 游戏结束覆盖层 ---------- */
#game-over-overlay {
display: none;
position: absolute;
top:0; left:0; width:100%; height:100%;
background: rgba(0,0,0,0.75);
backdrop-filter: blur(4px);
border-radius: 28px;
z-index: 200;
justify-content: center;
align-items: center;
flex-direction: column;
color: white;
text-align: center;
padding: 20px;
}
#game-over-overlay h1 { font-size:2.4rem; margin:0 0 4px 0; }
#game-over-overlay .final-score { font-size:3rem; font-weight:bold; color:#f1c40f; margin:4px 0; }
#game-over-overlay .high-score { font-size:1.3rem; color:#ecf0f1; margin:2px 0 14px 0; }
#game-over-overlay .high-score span { color:#f39c12; font-weight:bold; }
#game-over-overlay .btn-group {
display: flex;
gap: 14px;
margin-top: 10px;
flex-wrap: wrap;
justify-content: center;
}
#game-over-overlay .btn-group button {
background: #f39c12;
border: none;
border-radius: 36px;
padding: 14px 32px;
font-size: 1.2rem;
font-weight: bold;
color: white;
box-shadow: 0 5px 0 #d68910;
cursor: pointer;
font-family: inherit;
min-height: 52px;
min-width: 130px;
touch-action: manipulation;
}
#game-over-overlay .btn-group button:active {
transform: translateY(4px);
box-shadow: 0 1px 0 #d68910;
}
#game-over-overlay .btn-group button.back-btn {
background: #3498db;
box-shadow: 0 5px 0 #2471a3;
}
#game-over-overlay .btn-group button.back-btn:active {
box-shadow: 0 1px 0 #2471a3;
}
/* ---------- 答题覆盖层 ---------- */
#quiz-overlay {
display: none;
position: absolute;
top:0; left:0; width:100%; height:100%;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px);
border-radius: 28px;
z-index: 100;
justify-content: center;
align-items: center;
padding: 16px;
}
#quiz-box {
background: #ecf0f1;
border-radius: 36px;
padding: 24px 20px;
max-width: 360px;
width: 100%;
text-align: center;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
border: 4px solid #2c3e50;
}
#quiz-question { font-size:1.3rem; font-weight:bold; color:#2c3e50; margin:0 0 10px 0; line-height:1.4; }
#quiz-options {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.quiz-option {
background: white;
border: 3px solid #7f8c8d;
border-radius: 28px;
padding: 12px 5px;
font-size: 1rem;
font-weight: bold;
color: #2c3e50;
cursor: pointer;
touch-action: manipulation;
box-shadow: 0 4px 0 #95a5a6;
font-family: inherit;
min-height: 50px;
}
.quiz-option:active { transform: scale(0.94); box-shadow:0 1px 0 #95a5a6; }
.quiz-option.correct { background:#2ecc71; border-color:#27ae60; color:white; box-shadow:0 4px 0 #1e8449; }
.quiz-option.wrong { background:#e74c3c; border-color:#c0392b; color:white; box-shadow:0 4px 0 #922b21; }
.quiz-option.disabled { pointer-events:none; opacity:0.6; }
#quiz-result { font-size:1rem; font-weight:bold; min-height:2.5em; color:#2c3e50; background:white; border-radius:20px; padding:8px 12px; border:2px solid #7f8c8d; margin:8px 0; }
#quiz-continue {
background: #3498db;
border: none;
border-radius: 28px;
padding: 12px 0;
font-size: 1.2rem;
font-weight: bold;
color: white;
box-shadow: 0 4px 0 #2471a3;
cursor: pointer;
font-family: inherit;
min-height: 50px;
}
#quiz-continue:active { transform: translateY(4px); box-shadow:0 1px 0 #2471a3; }
#quiz-continue.hidden { display: none; }
.hidden { display: none !important; }
.fade-in { animation: fadeIn 0.3s ease; }
@keyframes fadeIn {
from { transform: scale(0.9); opacity:0; }
to { transform: scale(1); opacity:1; }
}
/* 响应式 */
@media (max-width:400px) {
#game-container { padding:4px; border-radius:24px; }
#player-bin { width:56px; height:68px; }
#player-bin .bin-body .bucket { width:48px; height:40px; }
#player-bin .bin-body .face { font-size:2.2rem; }
#player-bin .bin-body .lid { width:42px; height:14px; }
#player-bin .bin-body .lid::after { width:16px; height:6px; top:-8px; }
.garbage-item { font-size:2.2rem; width:46px; height:46px; }
#control-panel { height:120px; min-height:100px; gap:6px; }
#joystick-base { max-width:110px; }
.knob-btn { font-size:1.2rem; min-height:46px; }
.knob-btn .knob-icon { font-size:1.8rem; }
.knob-btn .knob-label { font-size:1.1rem; }
#top-bar { height:46px; padding:4px 10px; }
#timer-display { font-size:1.2rem; padding:2px 12px; min-width:50px; }
#score-display { font-size:1.3rem; padding:2px 14px; min-width:60px; }
#category-display { font-size:0.8rem; padding:2px 10px; }
.extra-btn { font-size:0.85rem; padding:4px 14px; min-height:34px; }
#quiz-question { font-size:1.1rem; }
#game-over-overlay h1 { font-size:2rem; }
#game-over-overlay .final-score { font-size:2.4rem; }
#game-over-overlay .btn-group button { padding:10px 22px; font-size:1rem; min-width:100px; min-height:44px; }
}
@media (max-height:640px) {
#control-panel { height:100px; min-height:80px; }
#joystick-base { max-width:100px; }
.knob-btn { min-height:40px; font-size:1rem; }
.knob-btn .knob-icon { font-size:1.6rem; }
.knob-btn .knob-label { font-size:1rem; }
#map-container { min-height:120px; }
#top-bar { height:40px; }
#timer-display { font-size:1rem; padding:2px 10px; }
#score-display { font-size:1.1rem; padding:2px 12px; }
}
</style>
</head>
<body>
<div id="game-container">
<!-- 选择界面 -->
<div id="select-screen">
<h1>垃圾小卫士</h1>
<div class="subtitle">⏱️ 60秒 · 双垃圾挑战</div>
<p>👋 选择你的垃圾桶类别</p>
<div class="select-bins">
<div class="select-bin bin-recyclable" data-cat="recyclable" onclick="window.startGame('recyclable')">
<span class="bin-emoji">♻️</span>
<span class="bin-label">可回收</span>
</div>
<div class="select-bin bin-hazardous" data-cat="hazardous" onclick="window.startGame('hazardous')">
<span class="bin-emoji">☣️</span>
<span class="bin-label">有害</span>
</div>
<div class="select-bin bin-kitchen" data-cat="kitchen" onclick="window.startGame('kitchen')">
<span class="bin-emoji">🍂</span>
<span class="bin-label">厨余</span>
</div>
<div class="select-bin bin-other" data-cat="other" onclick="window.startGame('other')">
<span class="bin-emoji">🗑️</span>
<span class="bin-label">其他</span>
</div>
</div>
<div class="high-score-display" id="select-high-score">🏆 最高分:0</div>
<div class="select-hint">🎮 左摇杆漫步 | 🍽️ 吃 / 💨 吐</div>
</div>
<!-- 游戏界面 -->
<div id="game-screen">
<!-- 顶部状态 -->
<div id="top-bar">
<div id="timer-display">⏱️ 60</div>
<div id="score-display">🏆 <span id="score-num">0</span></div>
<div id="category-display">♻️ 可回收</div>
</div>
<!-- 地图 -->
<div id="map-container">
<div id="player-bin">
<div class="bin-body">
<div class="lid"></div>
<div class="bucket">
<span class="face" id="bin-face">♻️</span>
<span class="stripe" id="stripe-icon">♻️</span>
<div class="wheel left"></div>
<div class="wheel right"></div>
</div>
</div>
<div class="mouth-icon" id="bin-mouth">😮</div>
<div class="stars" id="bin-stars">⭐✨</div>
</div>
<div id="interact-hint">💡 靠近垃圾,按 🍽️吃 或 💨吐</div>
<div id="feedback">👍 干得好!</div>
</div>
<!-- 控制面板 -->
<div id="control-panel">
<div id="joystick-area">
<div id="joystick-base">
<span class="joystick-arrow up">▲</span>
<span class="joystick-arrow down">▼</span>
<span class="joystick-arrow left">◀</span>
<span class="joystick-arrow right">▶</span>
<div id="joystick-thumb"></div>
</div>
</div>
<div id="knob-area">
<button class="knob-btn eat-btn" id="btn-eat">
<span class="knob-dots">
<span style="top:5px;left:20%;"></span>
<span style="top:5px;right:20%;"></span>
<span style="bottom:5px;left:20%;"></span>
<span style="bottom:5px;right:20%;"></span>
</span>
<span class="knob-icon">🍽️</span>
<span class="knob-label">吃</span>
</button>
<button class="knob-btn spit-btn" id="btn-spit">
<span class="knob-dots">
<span style="top:5px;left:20%;"></span>
<span style="top:5px;right:20%;"></span>
<span style="bottom:5px;left:20%;"></span>
<span style="bottom:5px;right:20%;"></span>
</span>
<span class="knob-icon">💨</span>
<span class="knob-label">吐</span>
</button>
</div>
</div>
<div id="bottom-extra">
<button class="extra-btn back" id="back-btn">🏠 返回</button>
<button class="extra-btn" id="pause-btn">⏸️ 暂停</button>
<button class="extra-btn reset" id="reset-btn">🔄 重来</button>
</div>
</div>
<!-- 游戏结束覆盖层 -->
<div id="game-over-overlay">
<h1>⏰ 时间到!</h1>
<div class="final-score" id="final-score-text">0 分</div>
<div class="high-score">🏆 历史最高分:<span id="final-high-score">0</span></div>
<div class="btn-group">
<button id="restart-same">🔄 再来一局</button>
<button class="back-btn" id="gameover-back">🏠 返回</button>
</div>
</div>
<!-- 答题覆盖层 -->
<div id="quiz-overlay">
<div id="quiz-box" class="fade-in">
<div id="quiz-question">❓ 吃剩的苹果核属于什么垃圾?</div>
<div id="quiz-options">
<button class="quiz-option" data-index="0">可回收</button>
<button class="quiz-option" data-index="1">有害</button>
<button class="quiz-option" data-index="2">厨余</button>
<button class="quiz-option" data-index="3">其他</button>
</div>
<div id="quiz-result">💡 选一个答案吧!</div>
<button id="quiz-continue" class="hidden">👉 继续</button>
</div>
</div>
</div>
<script>
(function() {
'use strict';
// ============================================================
// 1. 普通垃圾数据(30种)
// ============================================================
const GARBAGE = [
{ emoji: '🥤', name: '塑料瓶', cat: 'recyclable' },
{ emoji: '📰', name: '旧报纸', cat: 'recyclable' },
{ emoji: '🧃', name: '牛奶盒', cat: 'recyclable' },
{ emoji: '🧸', name: '旧布偶', cat: 'recyclable' },
{ emoji: '📦', name: '纸箱', cat: 'recyclable' },
{ emoji: '🥫', name: '易拉罐', cat: 'recyclable' },
{ emoji: '👕', name: '旧衣服', cat: 'recyclable' },
{ emoji: '🔩', name: '螺丝钉', cat: 'recyclable' },
{ emoji: '🔋', name: '电池', cat: 'hazardous' },
{ emoji: '💊', name: '过期药', cat: 'hazardous' },
{ emoji: '💡', name: '节能灯', cat: 'hazardous' },
{ emoji: '💄', name: '过期口红', cat: 'hazardous' },
{ emoji: '🖍️', name: '旧油画棒', cat: 'hazardous' },
{ emoji: '🪣', name: '油漆桶', cat: 'hazardous' },
{ emoji: '🌡️', name: '温度计', cat: 'hazardous' },
{ emoji: '🍎', name: '苹果核', cat: 'kitchen' },
{ emoji: '🌿', name: '枯树叶', cat: 'kitchen' },
{ emoji: '🍵', name: '茶叶渣', cat: 'kitchen' },
{ emoji: '🥕', name: '萝卜皮', cat: 'kitchen' },
{ emoji: '🍌', name: '香蕉皮', cat: 'kitchen' },
{ emoji: '🦐', name: '虾壳', cat: 'kitchen' },
{ emoji: '🥚', name: '蛋壳', cat: 'kitchen' },
{ emoji: '🌽', name: '玉米芯', cat: 'kitchen' },
{ emoji: '🦴', name: '大棒骨', cat: 'other' },
{ emoji: '🧻', name: '脏纸巾', cat: 'other' },
{ emoji: '🥡', name: '脏外卖盒', cat: 'other' },
{ emoji: '🧂', name: '碎陶瓷', cat: 'other' },
{ emoji: '💈', name: '头发', cat: 'other' },
{ emoji: '🪥', name: '旧牙刷', cat: 'other' },
{ emoji: '🧦', name: '破袜子', cat: 'other' },
{ emoji: '🩹', name: '创可贴', cat: 'other' },
];
// ============================================================
// 2. 丰富题库(50+ 环保相关题目)
// ============================================================
const ADVANCED_QUIZ = [
{ question: '吃剩的苹果核属于什么垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 2, explain: '苹果核是食物残渣,属于厨余垃圾。' },
{ question: '用完的电池应该扔进哪个垃圾桶?', options: ['可回收', '有害', '厨余', '其他'], answer: 1, explain: '电池含有重金属,是有害垃圾。' },
{ question: '干净的旧报纸是什么垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 0, explain: '报纸可以回收再利用,是可回收垃圾。' },
{ question: '用过的脏纸巾属于什么垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 3, explain: '脏纸巾被污染,无法回收,是其他垃圾。' },
{ question: '大棒骨属于什么垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 3, explain: '大棒骨太硬,不易腐烂,属于其他垃圾。' },
{ question: '过期药品应该扔到哪里?', options: ['可回收', '有害', '厨余', '其他'], answer: 1, explain: '过期药品有毒有害,是有害垃圾。' },
{ question: '喝完的牛奶盒(洗净)属于什么?', options: ['可回收', '有害', '厨余', '其他'], answer: 0, explain: '洗净的牛奶盒可以回收,是可回收垃圾。' },
{ question: '枯树叶属于什么垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 2, explain: '树叶容易腐烂,是厨余垃圾。' },
{ question: '油漆桶属于什么垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 1, explain: '油漆含有化学物质,是有害垃圾。' },
{ question: '旧衣服属于什么垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 0, explain: '旧衣服可以回收再利用,是可回收垃圾。' },
{ question: '用过的纸杯(内壁有塑料膜)属于什么垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 3, explain: '纸杯内壁的塑料膜难以分离,属于其他垃圾。' },
{ question: '破碎的陶瓷碗属于什么垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 3, explain: '陶瓷无法回收再利用,属于其他垃圾。' },
{ question: '过期化妆品属于什么垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 1, explain: '化妆品含有化学物质,属于有害垃圾。' },
{ question: '旧手机属于什么垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 0, explain: '旧手机含有贵金属,可以回收再利用。' },
{ question: '用过的创可贴属于什么垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 3, explain: '创可贴被污染后无法回收,属于其他垃圾。' },
{ question: '节能灯管属于什么垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 1, explain: '节能灯管含有汞等有害物质,属于有害垃圾。' },
{ question: '一次性塑料餐具(使用后)属于什么垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 3, explain: '使用后的一次性餐具被污染,属于其他垃圾。' },
{ question: '废玻璃瓶属于什么垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 0, explain: '玻璃可以回收再利用,属于可回收垃圾。' },
{ question: '下列哪种行为最有利于垃圾减量?', options: ['减少使用一次性物品', '多买包装精美的商品', '每天多扔垃圾', '把垃圾堆在一起'], answer: 0, explain: '减少使用一次性物品是垃圾减量的最有效方式。' },
{ question: '回收1吨废纸大约可以拯救多少棵树?', options: ['5棵', '17棵', '50棵', '100棵'], answer: 1, explain: '回收1吨废纸约可拯救17棵大树,节约资源。' },
{ question: '塑料瓶在自然环境中大约需要多少年才能分解?', options: ['50年', '200年', '500年', '1000年'], answer: 2, explain: '塑料瓶在自然环境中需要约500年才能分解。' },
{ question: '我国城市生活垃圾中占比最大的是哪类垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 2, explain: '厨余垃圾占比最大,约50%-60%。' },
{ question: '以下哪种垃圾可以转化为有机肥料?', options: ['废旧电池', '厨余垃圾', '旧衣服', '碎玻璃'], answer: 1, explain: '厨余垃圾可以通过堆肥转化为有机肥料。' },
{ question: '1吨废纸可以再造出多少好纸?', options: ['0.2吨', '0.5吨', '0.8吨', '1吨'], answer: 2, explain: '1吨废纸约可再造0.8吨好纸,节省资源。' },
{ question: '废旧电池对环境的危害主要是什么?', options: ['占用空间', '污染土壤和水源', '产生臭味', '影响美观'], answer: 1, explain: '电池中的重金属会污染土壤和水源,危害巨大。' },
{ question: '以下哪种做法可以减少白色污染?', options: ['多用塑料袋', '使用环保购物袋', '焚烧塑料', '填埋塑料'], answer: 1, explain: '使用环保购物袋能减少塑料袋的使用,从而减少白色污染。' },
{ question: '回收一个玻璃瓶节省的能量可以点亮一个灯泡多少小时?', options: ['1小时', '4小时', '10小时', '24小时'], answer: 1, explain: '回收一个玻璃瓶节省的能量约可点亮一个灯泡4小时。' },
{ question: '厨余垃圾在填埋场中会产生什么有害气体?', options: ['氧气', '二氧化碳', '甲烷', '氮气'], answer: 2, explain: '厨余垃圾分解会产生甲烷,是一种温室气体。' },
{ question: '哪种材质的包装最难降解?', options: ['纸', '玻璃', '塑料', '金属'], answer: 2, explain: '塑料在自然环境中极难降解,需要数百年。' },
{ question: '世界环境日是每年的几月几日?', options: ['3月12日', '4月22日', '5月31日', '6月5日'], answer: 3, explain: '世界环境日是每年的6月5日。' },
{ question: '以下哪种行为属于低碳生活?', options: ['使用一次性筷子', '骑自行车出行', '点外卖', '用塑料袋购物'], answer: 1, explain: '骑自行车出行减少碳排放,属于低碳生活。' },
{ question: '雾霾的主要成因之一是?', options: ['汽车尾气', '森林砍伐', '噪声污染', '光污染'], answer: 0, explain: '汽车尾气中的颗粒物是雾霾的重要成因。' },
{ question: '全球变暖的主要原因是?', options: ['二氧化碳排放增多', '臭氧层破坏', '太阳活动', '地壳运动'], answer: 0, explain: '二氧化碳等温室气体增多是导致全球变暖的主要原因。' },
{ question: '下列哪种能源属于可再生能源?', options: ['石油', '煤炭', '太阳能', '天然气'], answer: 2, explain: '太阳能是可再生的清洁能源。' },
{ question: '家庭中哪种做法最节约水资源?', options: ['用长流水洗菜', '使用节水龙头', '每天泡澡', '用水浇花'], answer: 1, explain: '使用节水龙头可以减少水资源浪费。' },
{ question: '以下哪种垃圾属于可回收物?', options: ['剩菜剩饭', '旧报纸', '废电池', '陶瓷碎片'], answer: 1, explain: '旧报纸属于可回收物。' },
{ question: '有害垃圾的收集容器通常是什么颜色?', options: ['蓝色', '红色', '绿色', '灰色'], answer: 1, explain: '有害垃圾通常用红色容器收集。' },
{ question: '厨余垃圾的收集容器通常是什么颜色?', options: ['蓝色', '红色', '绿色', '灰色'], answer: 2, explain: '厨余垃圾通常用绿色容器收集。' },
{ question: '可回收物的收集容器通常是什么颜色?', options: ['蓝色', '红色', '绿色', '灰色'], answer: 0, explain: '可回收物通常用蓝色容器收集。' },
{ question: '其他垃圾的收集容器通常是什么颜色?', options: ['蓝色', '红色', '绿色', '灰色'], answer: 3, explain: '其他垃圾通常用灰色或黑色容器收集。' },
{ question: '以下哪种行为有助于减少食物浪费?', options: ['购买过量食物', '合理规划餐食', '经常剩饭', '点餐必点大份'], answer: 1, explain: '合理规划餐食能减少食物浪费。' },
{ question: '回收1吨塑料瓶大约可以节约多少石油?', options: ['0.5吨', '1吨', '2吨', '5吨'], answer: 1, explain: '回收1吨塑料瓶约可节约1吨石油。' },
{ question: '哪种交通运输方式碳排放最低?', options: ['飞机', '汽车', '火车', '自行车'], answer: 3, explain: '自行车零排放,是最低碳的出行方式。' },
{ question: '垃圾分类的主要目的是?', options: ['减少垃圾总量', '实现资源回收和减量化', '美化城市环境', '增加就业岗位'], answer: 1, explain: '垃圾分类的核心目的是实现资源回收和垃圾减量化。' },
{ question: '废旧衣物属于哪类垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 0, explain: '干净的废旧衣物属于可回收物,可以回收再利用。' },
{ question: '旧鞋子属于哪类垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 0, explain: '旧鞋子如果材质可回收,属于可回收物。' },
{ question: '过期口红属于哪类垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 1, explain: '化妆品含有化学成分,属于有害垃圾。' },
{ question: 'X光片属于哪类垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 1, explain: 'X光片含有银等有害物质,属于有害垃圾。' },
{ question: '废弃农药瓶属于哪类垃圾?', options: ['可回收', '有害', '厨余', '其他'], answer: 1, explain: '农药瓶含有有毒化学物质,属于有害垃圾。' },
{ question: '盛放厨余垃圾的袋子的处理方式是?', options: ['直接投放', '破袋后投放', '投入其他垃圾桶', '随意丢弃'], answer: 1, explain: '厨余垃圾应破袋后投放,塑料袋属于其他垃圾。' }
];
// ---------- DOM ----------
const selectScreen = document.getElementById('select-screen');
const gameScreen = document.getElementById('game-screen');
const mapContainer = document.getElementById('map-container');
const feedback = document.getElementById('feedback');
const scoreNum = document.getElementById('score-num');
const timerDisplay = document.getElementById('timer-display');
const scoreDisplay = document.getElementById('score-display');
const categoryDisplay = document.getElementById('category-display');
const playerBin = document.getElementById('player-bin');
const binFace = document.getElementById('bin-face');
const stripeIcon = document.getElementById('stripe-icon');
const binMouth = document.getElementById('bin-mouth');
const binStars = document.getElementById('bin-stars');
const interactHint = document.getElementById('interact-hint');
const finalScoreText = document.getElementById('final-score-text');
const finalHighScore = document.getElementById('final-high-score');
const selectHighScore = document.getElementById('select-high-score');
const quizOverlay = document.getElementById('quiz-overlay');
const quizQuestion = document.getElementById('quiz-question');
const quizOptions = document.getElementById('quiz-options');
const quizResult = document.getElementById('quiz-result');
const quizContinue = document.getElementById('quiz-continue');
const gameOverOverlay = document.getElementById('game-over-overlay');
const restartSame = document.getElementById('restart-same');
const gameoverBack = document.getElementById('gameover-back');
const backBtn = document.getElementById('back-btn');
const pauseBtn = document.getElementById('pause-btn');
const resetBtn = document.getElementById('reset-btn');
const btnEat = document.getElementById('btn-eat');
const btnSpit = document.getElementById('btn-spit');
const joystickBase = document.getElementById('joystick-base');
const joystickThumb = document.getElementById('joystick-thumb');
// ---------- 颜色映射 ----------
const COLOR_MAP = {
'recyclable': { bucket: '#3498db', lid: '#2980b9', stripe: '♻️' },
'hazardous': { bucket: '#e74c3c', lid: '#c0392b', stripe: '☣️' },
'kitchen': { bucket: '#27ae60', lid: '#1e8449', stripe: '🍂' },
'other': { bucket: '#95a5a6', lid: '#7f8c8d', stripe: '🗑️' }
};
const EMOJI_MAP = {
'recyclable': '♻️',
'hazardous': '☣️',
'kitchen': '🍂',
'other': '🗑️'
};
// ---------- 状态 ----------
let selectedCat = '';
let score = 0;
let timeLeft = 60;
const maxTime = 60;
let isPaused = false;
let isGameOver = false;
let isQuizActive = false;
let isProcessing = false;
let garbageList = [];
let animationFrame = null;
let timerInterval = null;
let playerX = 30;
let playerY = 30;
const moveSpeed = 0.8;
let joystickActive = false;
let joystickDx = 0;
let joystickDy = 0;
let highScore = parseInt(localStorage.getItem('garbageEatHighScore')) || 0;
let currentCategory = '';
// 高级题库去重
let recentAdvancedIndices = [];
const MAX_RECENT = 5;
// ---------- 音效 ----------
let audioCtx = null;
function ensureAudio() {
if (!audioCtx) {
audioCtx = new(window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
return audioCtx;
}
function playCorrectSound() {
try {
const ctx = ensureAudio();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(880, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1320, ctx.currentTime + 0.08);
gain.gain.setValueAtTime(0.3, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.12);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.12);
const osc2 = ctx.createOscillator();
const gain2 = ctx.createGain();
osc2.connect(gain2);
gain2.connect(ctx.destination);
osc2.type = 'sine';
osc2.frequency.setValueAtTime(1320, ctx.currentTime);
osc2.frequency.exponentialRampToValueAtTime(1760, ctx.currentTime + 0.06);
gain2.gain.setValueAtTime(0.15, ctx.currentTime);
gain2.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.1);
osc2.start(ctx.currentTime + 0.02);
osc2.stop(ctx.currentTime + 0.1);
} catch (e) { /* 静默降级 */ }
}
function playWrongSound() {
try {
const ctx = ensureAudio();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(300, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(120, ctx.currentTime + 0.12);
gain.gain.setValueAtTime(0.2, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.14);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.14);
const osc2 = ctx.createOscillator();
const gain2 = ctx.createGain();
osc2.connect(gain2);
gain2.connect(ctx.destination);
osc2.type = 'square';
osc2.frequency.setValueAtTime(150, ctx.currentTime);
osc2.frequency.exponentialRampToValueAtTime(80, ctx.currentTime + 0.1);
gain2.gain.setValueAtTime(0.1, ctx.currentTime);
gain2.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.1);
osc2.start(ctx.currentTime + 0.02);
osc2.stop(ctx.currentTime + 0.1);
} catch (e) { /* 静默降级 */ }
}
// ---------- 工具 ----------
function getCategoryName(cat) {
const map = { 'recyclable': '可回收', 'hazardous': '有害', 'kitchen': '厨余', 'other': '其他' };
return map[cat] || '未知';
}
function getCategoryEmoji(cat) {
return EMOJI_MAP[cat] || '🗑️';
}
function getRandomGarbage() { return GARBAGE[Math.floor(Math.random() * GARBAGE.length)]; }
function updateScoreUI() {
scoreNum.textContent = score;
if (score < 0) {
scoreDisplay.classList.add('negative');
} else {
scoreDisplay.classList.remove('negative');
}
}
function updateTimerUI() {
timerDisplay.textContent = '⏱️ ' + Math.ceil(timeLeft);
if (timeLeft <= 10) {
timerDisplay.classList.add('warning');
} else {
timerDisplay.classList.remove('warning');
}
}
function updateHighScoreDisplay() {
selectHighScore.textContent = '🏆 最高分:' + highScore;
}
function showFeedback(emoji, text, isCorrect) {
feedback.textContent = `${emoji} ${text}`;
feedback.className = 'show';
feedback.style.background = isCorrect ? 'rgba(46, 204, 113, 0.85)' : 'rgba(231, 76, 60, 0.85)';
clearTimeout(feedback._hideTimer);
feedback._hideTimer = setTimeout(() => { feedback.className = ''; }, 800);
if (isCorrect) {
playCorrectSound();
} else {
playWrongSound();
}
}
// 不再使用表情变化,只控制嘴巴和星星
function setEating() {
binMouth.classList.add('show');
binStars.classList.add('show');
playerBin.classList.add('eating');
}
function setKicking() {
playerBin.classList.add('kicking');
}
function setHappy() {
binMouth.classList.remove('show');
binStars.classList.remove('show');
playerBin.classList.remove('eating', 'kicking');
}
// ---------- 更新垃圾桶外观 ----------
function updateBinAppearance(cat) {
const colors = COLOR_MAP[cat];
if (!colors) return;
const bucket = playerBin.querySelector('.bucket');
const lid = playerBin.querySelector('.lid');
bucket.style.background = colors.bucket;
lid.style.background = colors.lid;
// 正面大图标
binFace.textContent = getCategoryEmoji(cat);
// 条纹装饰
stripeIcon.textContent = colors.stripe;
// 顶部文字
categoryDisplay.textContent = getCategoryEmoji(cat) + ' ' + getCategoryName(cat);
}
// ---------- 判断特殊垃圾 ----------
function isSpecialGarbage(el) {
return el && el.dataset && el.dataset.special === 'true';
}
// ---------- 生成垃圾 ----------
function ensureTwoGarbage() {
if (isPaused || isGameOver || isQuizActive) return;
let normalCount = 0,
specialCount = 0;
garbageList.forEach(el => {
if (isSpecialGarbage(el)) specialCount++;
else normalCount++;
});
if (normalCount < 1) {
spawnSingleGarbage(false);
}
if (specialCount < 1) {
spawnSingleGarbage(true);
}
}
function spawnSingleGarbage(isSpecial) {
if (isPaused || isGameOver || isQuizActive) return;
let el;
if (isSpecial) {
el = document.createElement('div');
el.className = 'garbage-item special';
el.textContent = '❓';
el.dataset.cat = 'special';
el.dataset.name = '未知垃圾';
el.dataset.special = 'true';
const tag = document.createElement('span');
tag.className = 'special-tag';
tag.textContent = '❓ 特殊';
el.appendChild(tag);
} else {
const garbage = getRandomGarbage();
el = document.createElement('div');
el.className = 'garbage-item';
el.textContent = garbage.emoji;
el.dataset.cat = garbage.cat;
el.dataset.name = garbage.name;
el.dataset.special = 'false';
}
let x, y, attempts = 0;
let overlap = true;
const margin = 8;
while (overlap && attempts < 30) {
x = margin + Math.random() * (100 - 2 * margin);
y = margin + Math.random() * (100 - 2 * margin);
overlap = false;
for (let g of garbageList) {
const gx = parseFloat(g.style.left);
const gy = parseFloat(g.style.top);
if (Math.hypot(x - gx, y - gy) < 15) {
overlap = true;
break;
}
}
if (Math.hypot(x - playerX, y - playerY) < 20) overlap = true;
attempts++;
}
el.style.left = x + '%';
el.style.top = y + '%';
mapContainer.appendChild(el);
garbageList.push(el);
updateInteractHint();
}
// ---------- 更新交互提示 ----------
function updateInteractHint() {
if (garbageList.length === 0) {
interactHint.classList.remove('show', 'special-hint');
return;
}
let nearest = null;
let minDist = Infinity;
const binRect = playerBin.getBoundingClientRect();
const binCX = binRect.left + binRect.width / 2;
const binCY = binRect.top + binRect.height / 2;
for (let el of garbageList) {
const elRect = el.getBoundingClientRect();
const cx = elRect.left + elRect.width / 2;
const cy = elRect.top + elRect.height / 2;
const dist = Math.hypot(cx - binCX, cy - binCY);
if (dist < minDist) {
minDist = dist;
nearest = el;
}
}
const mapRect = mapContainer.getBoundingClientRect();
const threshold = Math.min(mapRect.width, mapRect.height) * 0.13;
if (nearest && minDist < threshold && !isProcessing) {
const isSpecial = isSpecialGarbage(nearest);
if (isSpecial) {
interactHint.textContent = '❓ 特殊垃圾,按"吃"挑战答题!(+30/-20)';
interactHint.classList.add('show', 'special-hint');
} else {
const catName = getCategoryName(nearest.dataset.cat);
interactHint.textContent = `💡 ${nearest.dataset.name}(${catName}),按 🍽️吃 或 💨吐`;
interactHint.classList.add('show');
interactHint.classList.remove('special-hint');
}
} else {
interactHint.classList.remove('show', 'special-hint');
}
}
// ---------- 吸入效果 ----------
function suckGarbageToBin(el) {
const binRect = playerBin.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
const mapRect = mapContainer.getBoundingClientRect();
const startX = (elRect.left + elRect.width / 2 - mapRect.left) / mapRect.width * 100;
const startY = (elRect.top + elRect.height / 2 - mapRect.top) / mapRect.height * 100;
const targetX = (binRect.left + binRect.width / 2 - mapRect.left) / mapRect.width * 100;
const targetY = (binRect.top + binRect.height / 2 - mapRect.top) / mapRect.height * 100;
el.style.transition =
'left 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), top 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.4s ease, opacity 0.3s ease';
el.style.left = targetX + '%';
el.style.top = targetY + '%';
el.style.transform = 'scale(0.2) rotate(720deg)';
el.style.opacity = '0.8';
el.classList.add('sucked');
const onFinish = () => {
el.removeEventListener('transitionend', onFinish);
if (el.parentNode) el.parentNode.removeChild(el);
const idx = garbageList.indexOf(el);
if (idx !== -1) garbageList.splice(idx, 1);
updateInteractHint();
ensureTwoGarbage();
};
el.addEventListener('transitionend', onFinish);
setTimeout(() => {
if (el.parentNode) {
el.removeEventListener('transitionend', onFinish);
el.parentNode.removeChild(el);
const idx = garbageList.indexOf(el);
if (idx !== -1) garbageList.splice(idx, 1);
updateInteractHint();
ensureTwoGarbage();
}
}, 600);
}
// ---------- 处理分类 ----------
function handleClassification(action) {
if (isPaused || isGameOver || isQuizActive || isProcessing) return;
if (garbageList.length === 0) {
showFeedback('🤔', '没有垃圾呀', false);
return;
}
let nearest = null;
let minDist = Infinity;
const binRect = playerBin.getBoundingClientRect();
const binCX = binRect.left + binRect.width / 2;
const binCY = binRect.top + binRect.height / 2;
for (let el of garbageList) {
const elRect = el.getBoundingClientRect();
const cx = elRect.left + elRect.width / 2;
const cy = elRect.top + elRect.height / 2;
const dist = Math.hypot(cx - binCX, cy - binCY);
if (dist < minDist) {
minDist = dist;
nearest = el;
}
}
if (!nearest) {
showFeedback('🤔', '没有垃圾呀', false);
return;
}
const mapRect = mapContainer.getBoundingClientRect();
const threshold = Math.min(mapRect.width, mapRect.height) * 0.13;
if (minDist > threshold) {
showFeedback('🚶', '靠近一点再操作', false);
return;
}
isProcessing = true;
const garbageEl = nearest;
const isSpecial = isSpecialGarbage(garbageEl);
if (isSpecial) {
if (action === true) {
// 吃 -> 触发答题
setEating();
suckGarbageToBin(garbageEl);
showFeedback('❓', '挑战高级答题!', true);
setTimeout(() => {
setHappy();
isProcessing = false;
triggerAdvancedQuiz();
}, 700);
} else {
// 吐
setKicking();
garbageEl.classList.add('flying');
setTimeout(() => {
if (garbageEl.parentNode) garbageEl.parentNode.removeChild(garbageEl);
const idx = garbageList.indexOf(garbageEl);
if (idx !== -1) garbageList.splice(idx, 1);
setHappy();
isProcessing = false;
updateInteractHint();
ensureTwoGarbage();
}, 800);
showFeedback('💨', '吐掉了,继续吧', false);
}
return;
}
// 普通垃圾
const cat = garbageEl.dataset.cat;
const name = garbageEl.dataset.name;
const actualIsCorrect = (cat === selectedCat);
const playerCorrect = (action === actualIsCorrect);
if (playerCorrect) {
if (actualIsCorrect) {
setEating();
suckGarbageToBin(garbageEl);
score += 10;
updateScoreUI();
showFeedback('🎉', `正确!+10分`, true);
setTimeout(() => setHappy(), 600);
} else {
setKicking();
garbageEl.classList.add('flying');
setTimeout(() => {
if (garbageEl.parentNode) garbageEl.parentNode.removeChild(garbageEl);
const idx = garbageList.indexOf(garbageEl);
if (idx !== -1) garbageList.splice(idx, 1);
setHappy();
updateInteractHint();
ensureTwoGarbage();
}, 800);
score += 5;
updateScoreUI();
showFeedback('👍', `判断正确!+5分`, true);
setTimeout(() => setHappy(), 500);
}
} else {
if (actualIsCorrect) {
setKicking();
garbageEl.classList.add('flying');
setTimeout(() => {
if (garbageEl.parentNode) garbageEl.parentNode.removeChild(garbageEl);
const idx = garbageList.indexOf(garbageEl);
if (idx !== -1) garbageList.splice(idx, 1);
setHappy();
updateInteractHint();
ensureTwoGarbage();
}, 800);
} else {
setEating();
suckGarbageToBin(garbageEl);
setTimeout(() => setHappy(), 600);
}
score -= 5;
updateScoreUI();
showFeedback('💥', `哎呀,扣5分`, false);
}
const cleanUp = () => {
if (!garbageEl.parentNode) {
isProcessing = false;
updateInteractHint();
ensureTwoGarbage();
} else {
setTimeout(cleanUp, 200);
}
};
setTimeout(cleanUp, 800);
}
// ---------- 触发高级答题(特殊垃圾专用,带去重) ----------
function triggerAdvancedQuiz() {
if (isQuizActive) return;
isQuizActive = true;
// 获取可用索引(排除最近使用的)
let availableIndices = [];
for (let i = 0; i < ADVANCED_QUIZ.length; i++) {
if (!recentAdvancedIndices.includes(i)) {
availableIndices.push(i);
}
}
// 如果所有题都被排除,重置历史
if (availableIndices.length === 0) {
recentAdvancedIndices = [];
availableIndices = ADVANCED_QUIZ.map((_, i) => i);
}
const randomIdx = availableIndices[Math.floor(Math.random() * availableIndices.length)];
recentAdvancedIndices.push(randomIdx);
if (recentAdvancedIndices.length > MAX_RECENT) {
recentAdvancedIndices.shift();
}
const q = ADVANCED_QUIZ[randomIdx];
quizQuestion.textContent = '🧠 ' + q.question;
const optionBtns = quizOptions.querySelectorAll('.quiz-option');
optionBtns.forEach((btn, idx) => {
btn.textContent = q.options[idx];
btn.dataset.index = idx;
btn.className = 'quiz-option';
btn.disabled = false;
btn.style.pointerEvents = 'auto';
});
quizResult.textContent = '💡 选一个答案吧!';
quizContinue.classList.add('hidden');
quizOverlay.style.display = 'flex';
quizOverlay.dataset.answer = q.answer;
quizOverlay.dataset.explain = q.explain;
quizOverlay.dataset.answered = 'false';
quizOverlay.dataset.isAdvanced = 'true';
}
// ---------- 答题选项点击 ----------
function handleQuizOptionClick(e) {
const btn = e.currentTarget;
if (btn.disabled) return;
const overlay = quizOverlay;
if (overlay.dataset.answered === 'true') return;
const selectedIndex = parseInt(btn.dataset.index);
const correctIndex = parseInt(overlay.dataset.answer);
const explain = overlay.dataset.explain;
const isCorrect = (selectedIndex === correctIndex);
const allBtns = quizOptions.querySelectorAll('.quiz-option');
allBtns.forEach(b => { b.disabled = true;
b.style.pointerEvents = 'none'; });
allBtns.forEach((b, idx) => {
if (idx === correctIndex) b.classList.add('correct');
else if (idx === selectedIndex && !isCorrect) b.classList.add('wrong');
});
const isAdvanced = overlay.dataset.isAdvanced === 'true';
if (isCorrect) {
let msg = '✅ 答对了!' + explain;
if (isAdvanced) {
score += 30;
msg = `🌟 答对了!+30分!` + explain;
} else {
score += 15;
msg = '✅ 答对了!+15分!' + explain;
}
quizResult.textContent = msg;
updateScoreUI();
playCorrectSound();
} else {
let msg = '❌ 正确答案是:' + allBtns[correctIndex].textContent + '。' + explain;
if (isAdvanced) {
score -= 20;
msg = `❌ 答错了,扣20分。正确答案是:` + allBtns[correctIndex].textContent + '。' + explain;
} else {
score -= 5;
msg = '❌ 答错了,扣5分。正确答案是:' + allBtns[correctIndex].textContent + '。' + explain;
}
quizResult.textContent = msg;
updateScoreUI();
playWrongSound();
}
overlay.dataset.answered = 'true';
overlay.dataset.quizResult = isCorrect ? 'pass' : 'fail';
quizContinue.classList.remove('hidden');
quizContinue.textContent = isCorrect ? '👉 继续!' : '😢 继续';
}
// ---------- 继续按钮 ----------
function handleQuizContinue() {
const overlay = quizOverlay;
const result = overlay.dataset.quizResult || 'pass';
overlay.style.display = 'none';
isQuizActive = false;
// 无论答对答错,都不再触发额外答题,直接继续游戏
setHappy();
if (!isGameOver) {
garbageList.forEach(el => { if (el.parentNode) el.parentNode.removeChild(el); });
garbageList = [];
updateInteractHint();
setTimeout(() => {
ensureTwoGarbage();
}, 400);
}
}
// ---------- 地图装饰 ----------
function buildMap() {
const toRemove = [];
mapContainer.querySelectorAll('.road-h, .road-v, .tree, .flower, .house').forEach(el => toRemove.push(el));
toRemove.forEach(el => el.remove());
const roads = [
{ type: 'h', top: 28, left: 0, width: 100 },
{ type: 'h', top: 58, left: 0, width: 100 },
{ type: 'v', top: 0, left: 28, height: 100 },
{ type: 'v', top: 0, left: 58, height: 100 },
];
roads.forEach(r => {
const div = document.createElement('div');
if (r.type === 'h') {
div.className = 'road-h';
div.style.top = r.top + '%';
div.style.left = r.left + '%';
div.style.width = r.width + '%';
} else {
div.className = 'road-v';
div.style.top = r.top + '%';
div.style.left = r.left + '%';
div.style.height = r.height + '%';
}
mapContainer.appendChild(div);
});
const trees = [
[6,6], [88,4], [5,44], [94,40], [12,74], [84,80],
[44,10], [56,6], [42,86], [64,84], [8,92], [90,90]
];
trees.forEach(([x,y]) => {
const el = document.createElement('div');
el.className = 'tree';
el.textContent = ['🌳','🌲','🌴'][Math.floor(Math.random()*3)];
el.style.left = x + '%';
el.style.top = y + '%';
el.style.fontSize = (1.8 + Math.random()*1.0) + 'rem';
mapContainer.appendChild(el);
});
for (let i=0; i<14; i++) {
const x = 5 + Math.random() * 90;
const y = 5 + Math.random() * 90;
const onRoad = (Math.abs(x-28)<14 || Math.abs(x-58)<14 || Math.abs(y-28)<14 || Math.abs(y-58)<14);
if (onRoad) continue;
const el = document.createElement('div');
el.className = 'flower';
el.textContent = ['🌸','🌺','🌻','🌷','🌼'][Math.floor(Math.random()*5)];
el.style.left = x + '%';
el.style.top = y + '%';
el.style.fontSize = (1.0 + Math.random()*0.8) + 'rem';
mapContainer.appendChild(el);
}
const houses = [
[3,3], [90,3], [3,90], [90,90]
];
houses.forEach(([x,y]) => {
const el = document.createElement('div');
el.className = 'house';
el.textContent = ['🏠','🏡','🏘️'][Math.floor(Math.random()*3)];
el.style.left = x + '%';
el.style.top = y + '%';
el.style.fontSize = (2.2 + Math.random()*0.6) + 'rem';
mapContainer.appendChild(el);
});
}
// ---------- 主循环 ----------
function gameLoop() {
if (!isPaused && !isGameOver && !isQuizActive) {
let dx = 0, dy = 0;
if (joystickActive) {
const magnitude = Math.hypot(joystickDx, joystickDy);
if (magnitude > 0.15) {
const speed = moveSpeed * Math.min(magnitude, 1);
dx = (joystickDx / magnitude) * speed;
dy = (joystickDy / magnitude) * speed;
}
}
const wasWalking = (dx !== 0 || dy !== 0);
if (wasWalking) {
let newX = playerX + dx;
let newY = playerY + dy;
const mapRect = mapContainer.getBoundingClientRect();
const binW = playerBin.offsetWidth || 64;
const binH = playerBin.offsetHeight || 76;
const maxX = 100 - (binW / mapRect.width) * 100;
const maxY = 100 - (binH / mapRect.height) * 100;
newX = Math.max(0, Math.min(maxX, newX));
newY = Math.max(0, Math.min(maxY, newY));
playerX = newX;
playerY = newY;
playerBin.style.left = playerX + '%';
playerBin.style.top = playerY + '%';
}
if (wasWalking && !isProcessing) {
playerBin.classList.add('walking');
} else {
playerBin.classList.remove('walking');
}
updateInteractHint();
ensureTwoGarbage();
}
animationFrame = requestAnimationFrame(gameLoop);
}
// ---------- 摇杆 ----------
function setupJoystick() {
let pointerId = null;
function getPos(e) {
const rect = joystickBase.getBoundingClientRect();
const cx = rect.left + rect.width/2;
const cy = rect.top + rect.height/2;
const radius = rect.width/2;
let dx = (e.clientX - cx) / radius;
let dy = (e.clientY - cy) / radius;
const mag = Math.hypot(dx, dy);
if (mag > 1) { dx /= mag; dy /= mag; }
return { dx, dy };
}
function updateThumb(dx, dy) {
const radius = joystickBase.offsetWidth/2;
const thumbRadius = joystickThumb.offsetWidth/2;
const maxDist = radius - thumbRadius - 2;
const mag = Math.hypot(dx, dy);
const clampedMag = Math.min(mag, 1);
const moveX = (dx / (mag||1)) * clampedMag * maxDist;
const moveY = (dy / (mag||1)) * clampedMag * maxDist;
joystickThumb.style.transform = `translate(calc(-50% + ${moveX}px), calc(-50% + ${moveY}px))`;
}
function onPointerDown(e) {
e.preventDefault();
if (pointerId !== null) return;
pointerId = e.pointerId;
joystickBase.setPointerCapture(pointerId);
joystickBase.classList.add('active');
const pos = getPos(e);
joystickDx = pos.dx;
joystickDy = pos.dy;
joystickActive = true;
updateThumb(joystickDx, joystickDy);
}
function onPointerMove(e) {
e.preventDefault();
if (pointerId === null) return;
const pos = getPos(e);
joystickDx = pos.dx;
joystickDy = pos.dy;
joystickActive = true;
updateThumb(joystickDx, joystickDy);
}
function onPointerUp(e) {
e.preventDefault();
if (pointerId === null) return;
pointerId = null;
joystickBase.classList.remove('active');
joystickDx = 0;
joystickDy = 0;
joystickActive = false;
joystickThumb.style.transform = 'translate(-50%, -50%)';
}
joystickBase.addEventListener('pointerdown', onPointerDown);
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', onPointerUp);
document.addEventListener('pointercancel', onPointerUp);
}
// ---------- 计时器 ----------
function startTimer() {
if (timerInterval) clearInterval(timerInterval);
timerInterval = setInterval(() => {
if (isPaused || isQuizActive) return;
timeLeft -= 0.1;
if (timeLeft < 0) timeLeft = 0;
updateTimerUI();
if (timeLeft <= 0) {
clearInterval(timerInterval);
timerInterval = null;
endGame();
}
}, 100);
}
// ---------- 结束游戏 ----------
function endGame() {
if (isGameOver) return;
isGameOver = true;
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
if (animationFrame) {
cancelAnimationFrame(animationFrame);
animationFrame = null;
}
garbageList.forEach(el => { if (el.parentNode) el.parentNode.removeChild(el); });
garbageList = [];
playerBin.classList.remove('walking');
interactHint.classList.remove('show', 'special-hint');
if (score > highScore) {
highScore = score;
localStorage.setItem('garbageEatHighScore', String(highScore));
}
updateHighScoreDisplay();
finalScoreText.textContent = score + ' 分';
finalHighScore.textContent = highScore;
gameOverOverlay.style.display = 'flex';
}
// ---------- 返回选择界面 ----------
function goToSelect() {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
if (animationFrame) {
cancelAnimationFrame(animationFrame);
animationFrame = null;
}
garbageList.forEach(el => { if (el.parentNode) el.parentNode.removeChild(el); });
garbageList = [];
isGameOver = true;
gameOverOverlay.style.display = 'none';
quizOverlay.style.display = 'none';
gameScreen.style.display = 'none';
selectScreen.style.display = 'flex';
updateHighScoreDisplay();
interactHint.classList.remove('show', 'special-hint');
recentAdvancedIndices = [];
}
function restartSameGame() {
gameOverOverlay.style.display = 'none';
recentAdvancedIndices = [];
if (currentCategory) {
startGame(currentCategory);
} else {
goToSelect();
}
}
// ---------- 启动游戏 ----------
function startGame(cat) {
currentCategory = cat;
selectedCat = cat;
updateBinAppearance(cat);
score = 0;
timeLeft = maxTime;
isGameOver = false;
isPaused = false;
isQuizActive = false;
isProcessing = false;
playerX = 30;
playerY = 30;
playerBin.style.left = playerX + '%';
playerBin.style.top = playerY + '%';
updateScoreUI();
updateTimerUI();
garbageList.forEach(el => { if (el.parentNode) el.parentNode.removeChild(el); });
garbageList = [];
quizOverlay.style.display = 'none';
gameOverOverlay.style.display = 'none';
feedback.className = '';
binMouth.classList.remove('show');
binStars.classList.remove('show');
playerBin.classList.remove('eating', 'kicking', 'walking');
setHappy();
interactHint.classList.remove('show', 'special-hint');
joystickDx = 0;
joystickDy = 0;
joystickActive = false;
joystickThumb.style.transform = 'translate(-50%, -50%)';
joystickBase.classList.remove('active');
recentAdvancedIndices = [];
selectScreen.style.display = 'none';
gameScreen.style.display = 'flex';
buildMap();
if (animationFrame) cancelAnimationFrame(animationFrame);
if (timerInterval) clearInterval(timerInterval);
gameLoop();
startTimer();
setTimeout(() => {
ensureTwoGarbage();
}, 400);
}
// ---------- 键盘快捷键 ----------
document.addEventListener('keydown', (e) => {
const key = e.key;
if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','w','W','a','A','s','S','d','D'].includes(key)) {
e.preventDefault();
let dx=0, dy=0;
if (key === 'ArrowUp' || key === 'w' || key === 'W') dy = -1;
if (key === 'ArrowDown' || key === 's' || key === 'S') dy = 1;
if (key === 'ArrowLeft' || key === 'a' || key === 'A') dx = -1;
if (key === 'ArrowRight' || key === 'd' || key === 'D') dx = 1;
joystickDx = dx;
joystickDy = dy;
joystickActive = true;
joystickBase.classList.add('active');
const radius = joystickBase.offsetWidth/2;
const thumbRadius = joystickThumb.offsetWidth/2;
const maxDist = radius - thumbRadius - 2;
const mag = Math.hypot(dx, dy);
const clampedMag = Math.min(mag, 1);
const moveX = (dx/(mag||1)) * clampedMag * maxDist;
const moveY = (dy/(mag||1)) * clampedMag * maxDist;
joystickThumb.style.transform = `translate(calc(-50% + ${moveX}px), calc(-50% + ${moveY}px))`;
}
if (key === '1') btnEat.click();
if (key === '2') btnSpit.click();
});
document.addEventListener('keyup', (e) => {
const key = e.key;
if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','w','W','a','A','s','S','d','D'].includes(key)) {
e.preventDefault();
joystickDx = 0;
joystickDy = 0;
joystickActive = false;
joystickBase.classList.remove('active');
joystickThumb.style.transform = 'translate(-50%, -50%)';
}
});
// ---------- 暴露全局 ----------
window.startGame = startGame;
// ---------- 事件绑定 ----------
btnEat.addEventListener('click', () => { handleClassification(true); });
btnSpit.addEventListener('click', () => { handleClassification(false); });
const optionBtns = quizOptions.querySelectorAll('.quiz-option');
optionBtns.forEach(btn => btn.addEventListener('click', handleQuizOptionClick));
quizContinue.addEventListener('click', handleQuizContinue);
restartSame.addEventListener('click', restartSameGame);
gameoverBack.addEventListener('click', goToSelect);
backBtn.addEventListener('click', goToSelect);
resetBtn.addEventListener('click', goToSelect);
pauseBtn.addEventListener('click', function() {
if (isGameOver || isQuizActive) return;
isPaused = !isPaused;
pauseBtn.textContent = isPaused ? '▶️ 继续' : '⏸️ 暂停';
if (isPaused) {
playerBin.classList.remove('walking');
} else {
if (!isGameOver) {
ensureTwoGarbage();
}
}
});
setupJoystick();
updateHighScoreDisplay();
selectScreen.style.display = 'flex';
gameScreen.style.display = 'none';
})();
</script>
</body>
</html>Game Source: 垃圾小卫士
Creator: MysticLion44
Libraries: none
Complexity: complex (2113 lines, 93.1 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-mysticlion44-mr6bujff" to link back to the original. Then publish at arcadelab.ai/publish.