垃圾小卫士
by MysticLion442381 lines90.5 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 #bdc3c7;
border: 3px solid #95a5a6;
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: 60px;
height: 68px;
z-index: 10;
pointer-events: none;
transition: left 0.08s linear, top 0.08s linear;
filter: drop-shadow(0 4px 8px 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: 52px;
height: 44px;
background: #4a90e2;
border-radius: 6px 6px 3px 3px;
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.2);
transition: background 0.3s;
}
#player-bin .bin-body .lid {
width: 46px;
height: 10px;
background: #7f8c8d;
border-radius: 10px 10px 0 0;
border: 3px solid #2c3e50;
border-bottom: none;
margin-bottom: -2px;
position: relative;
z-index: 2;
transition: transform 0.2s;
}
#player-bin .bin-body .wheel {
position: absolute;
bottom: -5px;
width: 13px;
height: 13px;
background: #2c3e50;
border-radius: 50%;
border: 2px solid #1a252f;
}
#player-bin .bin-body .wheel.left {
left: 2px;
}
#player-bin .bin-body .wheel.right {
right: 2px;
}
#player-bin .bin-body .face {
position: absolute;
font-size: 1.1rem;
top: 4px;
left: 50%;
transform: translateX(-50%);
line-height: 1;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
z-index: 3;
pointer-events: none;
transition: all 0.2s;
}
#player-bin .bin-body .cat-icon {
position: absolute;
font-size: 0.9rem;
bottom: 3px;
right: 3px;
z-index: 3;
pointer-events: none;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}
#player-bin .mouth-icon {
position: absolute;
font-size: 2.4rem;
top: -12px;
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: 1.8rem;
top: -20px;
right: -8px;
opacity: 0;
pointer-events: none;
z-index: 13;
transition: all 0.5s ease;
}
#player-bin .stars.show {
opacity: 1;
transform: translateY(-25px) 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: 50px;
height: 58px;
}
#player-bin .bin-body .bucket {
width: 44px;
height: 36px;
font-size: 1.2rem;
}
#player-bin .bin-body .lid {
width: 38px;
height: 8px;
}
#player-bin .bin-body .face {
font-size: 0.9rem;
}
.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="cat-icon" id="cat-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. 高级题库(垃圾分类知识 + 延伸问题,难度高)
// ============================================================
const ADVANCED_QUIZ = [{
question: '用过的纸杯(内壁有塑料膜)属于什么垃圾?',
options: ['可回收', '有害', '厨余', '其他'],
answer: 3,
explain: '纸杯内壁的塑料膜难以分离,属于其他垃圾。'
}, {
question: '破碎的陶瓷碗属于什么垃圾?',
options: ['可回收', '有害', '厨余', '其他'],
answer: 3,
explain: '陶瓷无法回收再利用,属于其他垃圾。'
}, {
question: '过期化妆品属于什么垃圾?',
options: ['可回收', '有害', '厨余', '其他'],
answer: 1,
explain: '化妆品含有化学物质,属于有害垃圾。'
}, {
question: '旧手机属于什么垃圾?',
options: ['可回收', '有害', '厨余', '其他'],
answer: 0,
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: '以下哪种垃圾可以转化为有机肥料?',
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 catIcon = document.getElementById('cat-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');
// ---------- 状态 ----------
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 = 3;
// ---------- 音效(Web Audio,简单短促) ----------
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) {
const map = { 'recyclable': '♻️', 'hazardous': '☣️', 'kitchen': '🍂', 'other': '🗑️' };
return 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 setFace(emoji) { binFace.textContent = emoji; }
function setHappy() { setFace('😊'); }
function setEating() { setFace('😮'); }
function setKicking() { setFace('😤'); }
// ---------- 判断是否为特殊垃圾 ----------
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();
binMouth.classList.add('show');
binStars.classList.add('show');
playerBin.classList.add('eating');
suckGarbageToBin(garbageEl);
showFeedback('❓', '挑战高级答题!', true);
setTimeout(() => {
binMouth.classList.remove('show');
binStars.classList.remove('show');
playerBin.classList.remove('eating');
setHappy();
}, 600);
setTimeout(() => {
isProcessing = false;
triggerAdvancedQuiz();
}, 700);
} else {
setKicking();
playerBin.classList.add('kicking');
garbageEl.classList.add('flying');
setTimeout(() => {
if (garbageEl.parentNode) garbageEl.parentNode.removeChild(garbageEl);
const idx = garbageList.indexOf(garbageEl);
if (idx !== -1) garbageList.splice(idx, 1);
playerBin.classList.remove('kicking');
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();
binMouth.classList.add('show');
binStars.classList.add('show');
playerBin.classList.add('eating');
suckGarbageToBin(garbageEl);
score += 10;
updateScoreUI();
showFeedback('🎉', `正确!+10分`, true);
setTimeout(() => {
binMouth.classList.remove('show');
binStars.classList.remove('show');
playerBin.classList.remove('eating');
setHappy();
}, 600);
} else {
setKicking();
playerBin.classList.add('kicking');
garbageEl.classList.add('flying');
setTimeout(() => {
if (garbageEl.parentNode) garbageEl.parentNode.removeChild(garbageEl);
const idx = garbageList.indexOf(garbageEl);
if (idx !== -1) garbageList.splice(idx, 1);
playerBin.classList.remove('kicking');
setHappy();
updateInteractHint();
ensureTwoGarbage();
}, 800);
score += 5;
updateScoreUI();
showFeedback('👍', `判断正确!+5分`, true);
setTimeout(() => setHappy(), 500);
}
} else {
if (actualIsCorrect) {
setKicking();
playerBin.classList.add('kicking');
garbageEl.classList.add('flying');
setTimeout(() => {
if (garbageEl.parentNode) garbageEl.parentNode.removeChild(garbageEl);
const idx = garbageList.indexOf(garbageEl);
if (idx !== -1) garbageList.splice(idx, 1);
playerBin.classList.remove('kicking');
setHappy();
updateInteractHint();
ensureTwoGarbage();
}, 800);
} else {
setEating();
binMouth.classList.add('show');
playerBin.classList.add('eating');
suckGarbageToBin(garbageEl);
setTimeout(() => {
binMouth.classList.remove('show');
playerBin.classList.remove('eating');
setHappy();
}, 600);
}
score -= 5;
updateScoreUI();
showFeedback('💥', `哎呀,扣5分`, false);
}
if (score < -20) {
setTimeout(() => {
triggerQuiz();
}, 700);
}
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 triggerQuiz() {
if (isQuizActive) return;
isQuizActive = true;
garbageList.forEach(el => { if (el.parentNode) el.parentNode.removeChild(el); });
garbageList = [];
updateInteractHint();
const q = ADVANCED_QUIZ[Math.floor(Math.random() * ADVANCED_QUIZ.length)];
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 = 'false';
}
// ---------- 答题选项点击 ----------
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;
if (score < -20 && !isGameOver) {
setTimeout(() => { triggerQuiz(); }, 500);
return;
}
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 || 60;
const binH = playerBin.offsetHeight || 68;
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;
const emoji = getCategoryEmoji(cat);
const name = getCategoryName(cat);
catIcon.textContent = emoji;
categoryDisplay.textContent = emoji + ' ' + name;
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 (2381 lines, 90.5 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-mr1rhd7x" to link back to the original. Then publish at arcadelab.ai/publish.