ซอร์สแพ็กเกจ · Belter Noun or Verb Skelter
CC BY 4.0 · โปรดให้เครดิต @joe · v1
นำไปใช้ รีมิกซ์ แล้วนำไปสอน
โค้ดด้านล่างเป็นเพียง HTML ทางเข้าที่เล่นได้ของแพ็กเกจ ดาวน์โหลด ZIP ทั้งชุดเพื่อแก้หรือรีมิกซ์เกมพร้อมไฟล์ประกอบแบบเส้นทางสัมพัทธ์ทั้งหมด
พรอมต์เริ่มต้นสำหรับแก้ด้วย AI
แก้เกมห้องเรียนนี้สำหรับกระดานโต้ตอบ คงเป้าแตะขนาดใหญ่และการเล่นทั้งห้อง ห้ามการติดตาม การเข้าสู่ระบบ การเก็บข้อมูลส่วนบุคคล และการเรียก API ระยะไกล ใช้ ZIP ที่ดาวน์โหลดทั้งชุด ไม่ใช่แค่ HTML ทางเข้าที่แสดงด้านล่าง และคงโครงสร้างโฟลเดอร์ที่พร้อมใช้ในเบราว์เซอร์ คงเป้าหมายทางการศึกษา ผู้สร้างต้นฉบับ: @joe บน ClassBoard Games (CC BY 4.0)
เฉพาะ HTML ทางเข้า · index.html · 2349.8 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Belter Noun or Verb Skelter — Kindergarten Conveyor Game</title>
<style>
:root {
--app-scale: 1;
--ink: #1E293B;
--noun-c: #2563EB;
--noun-dark: #1E3A8A;
--verb-c: #EA580C;
--verb-dark: #9A3412;
--ride-ms: 7000ms;
}
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
font-family: "Nunito", "Fredoka", "Comic Sans MS", "Trebuchet MS", system-ui, -apple-system, sans-serif;
background: radial-gradient(circle at 30% 18%, #38BDF8 0%, #1D4ED8 52%, #0F172A 100%);
color: var(--ink);
}
.viewport-shell {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
.stage-1280 {
width: 1280px;
height: 720px;
transform: scale(var(--app-scale));
transform-origin: center center;
background: linear-gradient(180deg, #BAE6FD 0%, #E0F2FE 54%, #BBF7D0 54%, #4ADE80 100%);
border-radius: 30px;
border: 6px solid #F59E0B;
box-shadow: 0 25px 65px rgba(15, 23, 42, 0.65), inset 0 2px 0 rgba(255,255,255,0.9);
display: flex;
flex-direction: column;
padding: 12px 16px;
gap: 9px;
position: relative;
overflow: hidden;
}
/* Decorative sky clouds & sun */
.sky-decor {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.sun-orb {
position: absolute;
top: 68px;
right: 36px;
font-size: 56px;
filter: drop-shadow(0 0 18px rgba(250, 204, 21, 0.8));
animation: gentleFloat 4s ease-in-out infinite;
}
@keyframes gentleFloat {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-6px) scale(1.04); }
}
/* ============ 1. TOP HUD BAR ============ */
.top-hud {
position: relative;
z-index: 10;
height: 58px;
background: linear-gradient(90deg, #1E3A8A 0%, #2563EB 45%, #EA580C 100%);
border-radius: 18px;
border: 3px solid #FDE047;
box-shadow: 0 6px 0 rgba(15, 23, 42, 0.35);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 14px;
color: #fff;
flex-shrink: 0;
}
.brand-left {
display: flex;
align-items: center;
gap: 10px;
}
.brand-icon {
width: 42px;
height: 42px;
border-radius: 13px;
background: linear-gradient(135deg, #FDE047, #F59E0B);
border: 3px solid #FFFBEB;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
box-shadow: 0 3px 0 rgba(120, 53, 15, 0.4);
}
.brand-title {
font-size: 21px;
font-weight: 900;
letter-spacing: 0.3px;
line-height: 1.05;
text-shadow: 0 2px 4px rgba(0,0,0,0.35);
}
.brand-sub {
font-size: 11px;
font-weight: 800;
color: #FEF08A;
}
/* Speed & Round Controls in HUD */
.hud-center {
display: flex;
align-items: center;
gap: 8px;
background: rgba(15, 23, 42, 0.32);
padding: 4px 8px;
border-radius: 14px;
border: 2px solid rgba(255,255,255,0.24);
}
.speed-btn {
border: none;
background: transparent;
color: #E0F2FE;
font-family: inherit;
font-size: 12.5px;
font-weight: 900;
padding: 5px 10px;
border-radius: 9px;
cursor: pointer;
transition: all 0.14s;
}
.speed-btn:hover { background: rgba(255,255,255,0.14); color: #fff; }
.speed-btn.active {
background: linear-gradient(180deg, #FDE047, #F59E0B);
color: #451A03;
box-shadow: 0 2px 0 #B45309;
}
.hud-sep {
width: 2px;
height: 22px;
background: rgba(255,255,255,0.25);
}
.action-chip {
border: 2px solid rgba(255,255,255,0.65);
background: rgba(255,255,255,0.18);
color: #fff;
font-family: inherit;
font-size: 12.5px;
font-weight: 900;
padding: 5px 10px;
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
}
.action-chip:hover { background: rgba(255,255,255,0.3); }
.action-chip.pause-active {
background: #EF4444;
border-color: #FCA5A5;
}
.hud-right {
display: flex;
align-items: center;
gap: 6px;
}
.util-btn {
border: 2px solid rgba(255,255,255,0.55);
background: rgba(255,255,255,0.16);
color: #fff;
font-family: inherit;
font-size: 12.5px;
font-weight: 900;
padding: 6px 10px;
border-radius: 11px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
}
.util-btn:hover { background: rgba(255,255,255,0.28); }
/* ============ 2. TEAM SCOREBOARD & TURN BANNER STRIP ============ */
.teams-strip {
position: relative;
z-index: 10;
height: 68px;
display: grid;
grid-template-columns: 1fr 310px;
gap: 10px;
flex-shrink: 0;
}
.team-cards-row {
display: grid;
grid-template-columns: repeat(var(--team-count, 3), 1fr);
gap: 8px;
}
.team-score-card {
background: rgba(255, 255, 255, 0.94);
border: 3.5px solid #CBD5E1;
border-radius: 16px;
padding: 6px 10px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
cursor: pointer;
transition: transform 0.16s, box-shadow 0.16s, border-color 0.16s;
}
.team-score-card.current-turn {
transform: translateY(-2px) scale(1.01);
box-shadow: 0 6px 0 rgba(0,0,0,0.2), 0 0 0 4px #FDE047;
background: #FFFFFF;
}
.team-score-card.bump {
animation: scoreBump 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes scoreBump {
0%, 100% { transform: scale(1); }
45% { transform: scale(1.12); }
}
.tsc-left {
display: flex;
align-items: center;
gap: 8px;
}
.tsc-avatar {
width: 42px;
height: 42px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
color: #fff;
font-weight: 900;
box-shadow: inset 0 -3px 0 rgba(0,0,0,0.2);
}
.tsc-name {
font-size: 14.5px;
font-weight: 900;
color: #0F172A;
line-height: 1.1;
}
.tsc-sub {
font-size: 11px;
font-weight: 800;
color: #475569;
margin-top: 2px;
}
.tsc-right {
text-align: right;
}
.tsc-pts {
font-size: 24px;
font-weight: 900;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.tsc-turn-pill {
display: inline-block;
font-size: 10px;
font-weight: 900;
padding: 2px 7px;
border-radius: 999px;
background: #FDE047;
color: #451A03;
text-transform: uppercase;
}
/* Round Progress & Sorted Bins Box */
.round-progress-card {
background: rgba(255, 255, 255, 0.94);
border: 3.5px solid #93C5FD;
border-radius: 16px;
padding: 6px 12px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
}
.rp-top {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12.5px;
font-weight: 900;
color: #1E3A8A;
}
.rp-bins {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.rp-bin-badge {
flex: 1;
text-align: center;
padding: 3px 6px;
border-radius: 9px;
font-size: 12px;
font-weight: 900;
}
.rp-bin-badge.n { background: #DBEAFE; color: #1E40AF; border: 1.5px solid #93C5FD; }
.rp-bin-badge.v { background: #FFEDD5; color: #9A3412; border: 1.5px solid #FDBA74; }
/* ============ 3. CONVEYOR BELT & HELTER-SKELTER FACTORY STAGE ============ */
.conveyor-arena {
position: relative;
z-index: 10;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 10px;
}
/* Upper Conveyor Machine Box */
.machine-frame {
position: relative;
flex: 1;
background: linear-gradient(180deg, rgba(255,255,255,0.88) 0%, rgba(240,249,255,0.92) 72%, #94A3B8 72%, #64748B 100%);
border-radius: 26px;
border: 5px solid #475569;
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.22), inset 0 2px 0 #fff;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
.machine-frame.shake {
animation: machineShake 0.45s ease;
}
@keyframes machineShake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-11px); }
40% { transform: translateX(11px); }
60% { transform: translateX(-7px); }
80% { transform: translateX(7px); }
}
/* Top Draining Countdown Timer Bar */
.timer-bar-wrap {
height: 16px;
width: 100%;
background: #E2E8F0;
border-bottom: 3px solid #475569;
position: relative;
overflow: hidden;
}
.timer-bar-fill {
height: 100%;
width: 100%;
background: linear-gradient(90deg, #EF4444 0%, #F59E0B 45%, #10B981 100%);
transform-origin: left center;
}
.timer-bar-fill.draining {
animation: drainClock var(--ride-ms) linear forwards;
}
.timer-bar-fill.paused {
animation-play-state: paused !important;
}
@keyframes drainClock {
from { transform: scaleX(1); }
to { transform: scaleX(0); }
}
/* Turn &Worked Sentence Reveal Header inside Machine */
.machine-header-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 18px 0;
position: relative;
z-index: 6;
}
.turn-banner-pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 5px 15px;
border-radius: 999px;
background: #1E293B;
color: #FEF08A;
font-size: 15px;
font-weight: 900;
border: 3px solid #FACC15;
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
}
.sweet-spot-hint {
font-size: 13px;
font-weight: 900;
color: #0F172A;
background: rgba(254, 240, 138, 0.9);
border: 2px solid #F59E0B;
padding: 4px 14px;
border-radius: 999px;
}
/* Worked sentence reveal overlay pill */
.reveal-card {
position: absolute;
top: 46px;
left: 50%;
transform: translateX(-50%);
z-index: 15;
background: #FFFFFF;
border: 4px solid #F59E0B;
border-radius: 20px;
padding: 6px 22px;
display: none;
align-items: center;
gap: 12px;
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.25);
white-space: nowrap;
}
.reveal-card.show {
display: inline-flex;
animation: popIn 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes popIn {
0% { transform: translateX(-50%) scale(0.7); opacity: 0; }
100% { transform: translateX(-50%) scale(1); opacity: 1; }
}
.rv-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 16px;
font-weight: 900;
color: #fff;
}
.rv-badge.noun { background: var(--noun-c); }
.rv-badge.verb { background: var(--verb-c); }
.rv-sentence {
font-size: 19px;
font-weight: 900;
color: #1E293B;
}
/* Left & Right Factory Hatch Doors + Sweet Spot Arch */
.belt-viewport {
position: relative;
flex: 1;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
/* Golden Sweet Spot Target Zone in the middle of the belt */
.sweet-spot-zone {
position: absolute;
top: 10px;
bottom: 6px;
width: 340px;
left: 50%;
transform: translateX(-50%);
border: 3px dashed rgba(245, 158, 11, 0.55);
background: radial-gradient(circle, rgba(254, 240, 138, 0.28) 0%, transparent 75%);
border-radius: 22px;
pointer-events: none;
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: 6px;
z-index: 1;
}
.sweet-spot-zone span {
font-size: 11px;
font-weight: 900;
color: #B45309;
letter-spacing: 0.6px;
text-transform: uppercase;
background: rgba(254, 243, 199, 0.9);
padding: 2px 10px;
border-radius: 999px;
}
/* Left & Right Helter-Skelter Side Chute Portals */
.side-portal {
position: absolute;
top: 12px;
bottom: 8px;
width: 96px;
z-index: 4;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-weight: 900;
font-size: 12px;
text-align: center;
pointer-events: none;
}
.side-portal.left-chute {
left: 10px;
background: linear-gradient(90deg, rgba(239, 68, 68, 0.22), transparent);
border-left: 5px solid #EF4444;
border-radius: 14px 0 0 14px;
color: #991B1B;
}
.side-portal.right-entry {
right: 10px;
background: linear-gradient(-90deg, rgba(37, 99, 235, 0.2), transparent);
border-right: 5px solid #3B82F6;
border-radius: 0 14px 14px 0;
color: #1E3A8A;
}
/* THE MOVING RIDER & CRATE ON THE CONVEYOR BELT! */
.crate-rider {
position: relative;
z-index: 8;
will-change: transform;
}
/* Just like original conveyor-quest-kindergarten:
Enters from right (+520px), slows through center so children can read it,
then exits off the left edge (-560px)! */
.crate-rider.riding {
animation: rideBeltAcross var(--ride-ms) linear forwards;
}
.crate-rider.paused {
animation-play-state: paused !important;
}
@keyframes rideBeltAcross {
0% { transform: translateX(520px); }
18% { transform: translateX(170px); }
50% { transform: translateX(0px); }
82% { transform: translateX(-170px); }
100% { transform: translateX(-560px); }
}
.word-crate {
width: 268px;
height: 162px;
background: linear-gradient(180deg, #FFFBEB 0%, #FDE68A 100%);
border: 5px solid #B45309;
border-radius: 26px;
box-shadow: 0 10px 0 #78350F, 0 14px 28px rgba(0,0,0,0.25);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
.word-crate.right-noun {
background: linear-gradient(180deg, #DBEAFE, #93C5FD);
border-color: #1D4ED8;
box-shadow: 0 10px 0 #1E3A8A, 0 0 25px rgba(37, 99, 235, 0.55);
}
.word-crate.right-verb {
background: linear-gradient(180deg, #FFEDD5, #FDBA74);
border-color: #C2410C;
box-shadow: 0 10px 0 #7C2D12, 0 0 25px rgba(234, 88, 12, 0.55);
}
.word-crate.wrong-hit {
background: linear-gradient(180deg, #FEE2E2, #FCA5A5);
border-color: #DC2626;
box-shadow: 0 10px 0 #7F1D1D;
}
.crate-inner-frame {
position: absolute;
inset: 7px;
border: 2.5px dashed rgba(180, 83, 9, 0.32);
border-radius: 18px;
pointer-events: none;
}
.crate-q-badge {
position: absolute;
top: -12px;
right: -12px;
width: 36px;
height: 36px;
border-radius: 50%;
background: #F59E0B;
border: 3px solid #FFF;
color: #fff;
font-size: 20px;
font-weight: 900;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.crate-pic {
font-size: 68px;
line-height: 1;
filter: drop-shadow(0 4px 4px rgba(0,0,0,0.16));
}
.crate-word-txt {
font-size: 40px;
font-weight: 900;
color: #0F172A;
line-height: 1.05;
margin-top: 2px;
letter-spacing: 0.5px;
}
/* Stamp Overlay on Crate */
.verdict-stamp {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 12;
animation: stampSlam 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
@keyframes stampSlam {
0% { transform: scale(2.2) rotate(-14deg); opacity: 0; }
100% { transform: scale(1) rotate(-6deg); opacity: 1; }
}
.stamp-pill {
padding: 6px 20px;
border-radius: 16px;
border: 4px solid #FFFFFF;
font-size: 28px;
font-weight: 900;
color: #FFFFFF;
box-shadow: 0 8px 20px rgba(0,0,0,0.35);
}
.stamp-pill.ok { background: #16A34A; }
.stamp-pill.no { background: #DC2626; }
/* Floating Points (+95 🔥) */
.floating-score {
position: absolute;
top: -24px;
left: 50%;
transform: translateX(-50%);
font-size: 30px;
font-weight: 900;
color: #15803D;
background: #FEF08A;
border: 3px solid #F59E0B;
padding: 3px 14px;
border-radius: 999px;
box-shadow: 0 6px 14px rgba(0,0,0,0.2);
z-index: 20;
white-space: nowrap;
animation: floatUpPts 1.1s ease-out forwards;
}
.floating-score.zero {
color: #B91C1C;
background: #FEE2E2;
border-color: #EF4444;
}
@keyframes floatUpPts {
0% { opacity: 0; transform: translate(-50%, 10px) scale(0.7); }
25% { opacity: 1; transform: translate(-50%, -12px) scale(1.08); }
100% { opacity: 0; transform: translate(-50%, -56px) scale(0.95); }
}
/* Get-Ready Turn Countdown Overlay inside Belt Viewport */
.get-ready-card {
position: absolute;
z-index: 14;
background: rgba(255, 255, 255, 0.97);
border: 5px solid #F59E0B;
border-radius: 26px;
padding: 14px 34px;
text-align: center;
box-shadow: 0 16px 36px rgba(15, 23, 42, 0.3);
animation: popInCenter 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes popInCenter {
0% { transform: scale(0.6); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
/* Conveyor Belt Surface & Rollers */
.conveyor-deck {
height: 54px;
width: 100%;
background: #334155;
border-top: 6px solid #1E293B;
position: relative;
display: flex;
align-items: center;
justify-content: space-around;
padding: 0 24px;
flex-shrink: 0;
}
.conveyor-treads {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 11px;
background: repeating-linear-gradient(
90deg,
#FACC15 0px,
#FACC15 24px,
#475569 24px,
#475569 48px
);
animation: beltMoveLeft 0.65s linear infinite;
}
.conveyor-treads.paused {
animation-play-state: paused !important;
}
@keyframes beltMoveLeft {
from { background-position: 0 0; }
to { background-position: -48px 0; }
}
.roller-wheel {
width: 32px;
height: 32px;
border-radius: 50%;
background: radial-gradient(circle, #FDE047 30%, #94A3B8 34%, #475569 100%);
border: 3.5px solid #0F172A;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
animation: wheelSpinLeft 1.2s linear infinite;
z-index: 2;
}
.roller-wheel.paused {
animation-play-state: paused !important;
}
@keyframes wheelSpinLeft {
from { transform: rotate(360deg); }
to { transform: rotate(0deg); }
}
/* ============ 4. TWO GIANT HELTER-SKELTER ANSWER BUTTONS ============ */
.answer-chutes-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
height: 158px;
flex-shrink: 0;
}
.ans-chute-btn {
border-radius: 26px;
border: 6px solid;
font-family: inherit;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 22px;
position: relative;
transition: transform 0.12s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.12s, filter 0.12s;
}
.ans-chute-btn:hover:not(:disabled) {
transform: translateY(-4px) scale(1.01);
}
.ans-chute-btn:active:not(:disabled) {
transform: translateY(5px);
}
.ans-chute-btn:disabled {
opacity: 0.72;
cursor: default;
}
.ans-chute-btn.noun {
background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 65%, #1E3A8A 100%);
border-color: #93C5FD;
color: #FFFFFF;
box-shadow: 0 10px 0 #172554;
}
.ans-chute-btn.verb {
background: linear-gradient(135deg, #EA580C 0%, #C2410C 65%, #7C2D12 100%);
border-color: #FDBA74;
color: #FFFFFF;
box-shadow: 0 10px 0 #431407;
}
.ans-chute-btn.hit-right {
box-shadow: 0 0 0 6px #4ADE80, 0 10px 0 #14532D !important;
filter: brightness(1.12);
}
.ans-chute-btn.hit-wrong {
box-shadow: 0 0 0 6px #EF4444, 0 10px 0 #7F1D1D !important;
filter: grayscale(0.35);
}
.ac-slide-badge {
width: 92px;
height: 92px;
border-radius: 22px;
background: rgba(255, 255, 255, 0.2);
border: 3px solid rgba(255, 255, 255, 0.6);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 42px;
flex-shrink: 0;
}
.ac-text {
flex: 1;
padding: 0 16px;
text-align: left;
}
.ac-title {
font-size: 42px;
font-weight: 900;
line-height: 1;
letter-spacing: 0.8px;
text-shadow: 0 3px 6px rgba(0,0,0,0.3);
}
.ac-desc {
font-size: 15px;
font-weight: 800;
margin-top: 5px;
color: #FEF08A;
}
.ac-examples {
display: inline-block;
margin-top: 6px;
background: rgba(15, 23, 42, 0.32);
padding: 3px 12px;
border-radius: 999px;
font-size: 13px;
font-weight: 800;
color: #fff;
}
.key-hint-pill {
position: absolute;
top: 10px;
right: 14px;
background: rgba(255,255,255,0.22);
border: 1.5px solid rgba(255,255,255,0.5);
padding: 2px 9px;
border-radius: 8px;
font-size: 11.5px;
font-weight: 900;
}
/* ============ 5. START / SETUP LAUNCH OVERLAY & PODIUM MODAL ============ */
.overlay-screen {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.76);
backdrop-filter: blur(6px);
z-index: 60;
display: none;
align-items: center;
justify-content: center;
padding: 20px;
}
.overlay-screen.open {
display: flex;
}
.dialog-card {
width: 100%;
max-width: 720px;
background: linear-gradient(180deg, #FFFBEB 0%, #EFF6FF 100%);
border: 6px solid #F59E0B;
border-radius: 30px;
padding: 24px 30px;
box-shadow: 0 25px 65px rgba(0,0,0,0.55);
text-align: center;
}
.setup-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin: 16px 0;
}
.setup-box {
background: #FFFFFF;
border: 3px solid #93C5FD;
border-radius: 18px;
padding: 12px;
}
.sb-title {
font-size: 13.5px;
font-weight: 900;
color: #1E3A8A;
margin-bottom: 8px;
}
.sb-opts {
display: flex;
gap: 6px;
justify-content: center;
}
.sb-opt {
flex: 1;
border: 2.5px solid #CBD5E1;
background: #F8FAFC;
color: #334155;
font-family: inherit;
font-size: 13px;
font-weight: 900;
padding: 7px 6px;
border-radius: 11px;
cursor: pointer;
}
.sb-opt.active {
background: #2563EB;
border-color: #1E3A8A;
color: #fff;
box-shadow: 0 3px 0 #1E3A8A;
}
.big-start-btn {
border: 4px solid #B45309;
background: linear-gradient(180deg, #FDE047 0%, #F59E0B 100%);
color: #451A03;
font-family: inherit;
font-size: 26px;
font-weight: 900;
padding: 12px 36px;
border-radius: 22px;
box-shadow: 0 8px 0 #92400E;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 10px;
transition: transform 0.12s;
}
.big-start-btn:hover { transform: translateY(-2px) scale(1.02); }
.big-start-btn:active { transform: translateY(4px); box-shadow: 0 3px 0 #92400E; }
/* Podium Rows in End-of-Round Screen */
.podium-list {
display: flex;
flex-direction: column;
gap: 8px;
margin: 12px 0;
text-align: left;
}
.podium-row {
background: #FFFFFF;
border: 3px solid #93C5FD;
border-radius: 16px;
padding: 8px 14px;
display: flex;
align-items: center;
justify-content: space-between;
}
.missed-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: center;
margin-top: 6px;
}
.missed-chip {
border: 2px solid #CBD5E1;
background: #FFFFFF;
padding: 4px 10px;
border-radius: 999px;
font-size: 13px;
font-weight: 900;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 5px;
}
/* Confetti Layer */
.confetti-layer {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 70;
overflow: hidden;
}
.confetti-dot {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
animation: burstOut 0.9s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes burstOut {
0% { transform: translate(0, 0) scale(1); opacity: 1; }
100% { transform: translate(var(--tx), var(--ty)) scale(0.1); opacity: 0; }
}
</style>
</head>
<body>
<div class="viewport-shell">
<div class="stage-1280" id="appStage">
<div class="sky-decor">
<div class="sun-orb">☀️</div>
</div>
<!-- 1. TOP HUD BAR -->
<header class="top-hud">
<div class="brand-left">
<div class="brand-icon">🎢</div>
<div>
<div class="brand-title">Belter Noun or Verb Skelter</div>
<div class="brand-sub">Catch Each Word Crate Before It Rides Off the Belt!</div>
</div>
</div>
<div class="hud-center">
<button class="speed-btn" data-ms="10000" type="button">🐢 Slow 10s</button>
<button class="speed-btn active" data-ms="7000" type="button">🚶 Normal 7s</button>
<button class="speed-btn" data-ms="5000" type="button">🚀 Fast 5s</button>
<div class="hud-sep"></div>
<button class="action-chip" id="pauseBeltBtn" type="button">⏸️ Pause Belt</button>
<button class="action-chip" id="swapWordBtn" type="button" title="Deal a different word to the same team">🔁 Swap Word</button>
<button class="action-chip" id="finishRoundBtn" type="button">🏁 Podium</button>
</div>
<div class="hud-right">
<button class="util-btn" id="repeatBtn" type="button"><span>🔊</span> Repeat</button>
<button class="util-btn" id="setupBtn" type="button"><span>⚙️</span> Setup</button>
<button class="util-btn" id="soundBtn" type="button"><span id="soundIcon">🔊</span> Sound</button>
<button class="util-btn" id="fullBtn" type="button"><span>⛶</span> Full</button>
</div>
</header>
<!-- 2. TEAM SCOREBOARD & BINS -->
<section class="teams-strip">
<div class="team-cards-row" id="teamCardsRow" style="--team-count: 3"></div>
<div class="round-progress-card">
<div class="rp-top">
<span id="roundLabelTxt">Round 1 • Word 1 / 18</span>
<span style="color:#D97706">⚡ Speed Bonus ON</span>
</div>
<div class="rp-bins">
<div class="rp-bin-badge n">🧱 Nouns: <b id="nounBinCount">0</b></div>
<div class="rp-bin-badge v">🏃 Verbs: <b id="verbBinCount">0</b></div>
</div>
</div>
</section>
<!-- 3. MAIN CONVEYOR MACHINE + TWO HELTER-SKELTER CHUTE BUTTONS -->
<main class="conveyor-arena">
<div class="machine-frame" id="machineFrame">
<!-- Draining Clock Bar -->
<div class="timer-bar-wrap">
<div class="timer-bar-fill" id="timerBarFill"></div>
</div>
<!-- Turn & Sweet Spot Row -->
<div class="machine-header-row">
<div class="turn-banner-pill" id="turnBannerPill">
<span>🦊</span> <span>Team Fox's Turn!</span>
</div>
<div style="display:flex;gap:8px">
<button class="action-chip" id="ruleNounChip" type="button" style="background:#EFF6FF;color:#1E40AF;border-color:#93C5FD">
🔊 What is a Noun?
</button>
<button class="action-chip" id="ruleVerbChip" type="button" style="background:#FFF7ED;color:#9A3412;border-color:#FDBA74">
🔊 What is a Verb?
</button>
</div>
<div class="sweet-spot-hint">
🎯 Catch early for up to <b>+100 pts</b> + <b>🔥 Streak Bonus</b>!
</div>
</div>
<!-- Worked Sentence Reveal Popup -->
<div class="reveal-card" id="revealCard">
<span class="rv-badge noun" id="revealBadge">🧱 NOUN!</span>
<span class="rv-sentence" id="revealSentence">Look at the fluffy cat!</span>
</div>
<!-- Conveyor Belt Viewport -->
<div class="belt-viewport" id="beltViewport">
<div class="side-portal left-chute">
<span style="font-size:26px">⚠️</span>
<span>EDGE DROP</span>
</div>
<div class="sweet-spot-zone">
<span>⭐ Sweet Spot Reading Zone ⭐</span>
</div>
<div class="side-portal right-entry">
<span style="font-size:26px">📦</span>
<span>CRATE IN</span>
</div>
<!-- Get-Ready Turn Card -->
<div class="get-ready-card" id="getReadyCard" style="display:none">
<div style="font-size:46px;line-height:1" id="grIcon">🦊</div>
<div style="font-size:26px;font-weight:900;color:#1E293B;margin-top:4px" id="grTitle">Team Fox's Turn!</div>
<div style="font-size:14px;font-weight:800;color:#D97706">Watch the belt and press NOUN or VERB!</div>
</div>
<!-- Moving Crate Rider -->
<div class="crate-rider" id="crateRider">
<div class="word-crate" id="wordCrate" title="Tap crate to hear word again!">
<div class="crate-inner-frame"></div>
<div class="crate-q-badge" id="crateQBadge">?</div>
<div class="crate-pic" id="cratePic">🐱</div>
<div class="crate-word-txt" id="crateWordTxt">cat</div>
</div>
</div>
</div>
<!-- Moving Conveyor Belt Treads & Spinning Rollers -->
<div class="conveyor-deck">
<div class="conveyor-treads" id="conveyorTreads"></div>
<div class="roller-wheel">⚙️</div>
<div class="roller-wheel">⚙️</div>
<div class="roller-wheel">⚙️</div>
<div class="roller-wheel">⚙️</div>
<div class="roller-wheel">⚙️</div>
<div class="roller-wheel">⚙️</div>
<div class="roller-wheel">⚙️</div>
<div class="roller-wheel">⚙️</div>
<div class="roller-wheel">⚙️</div>
</div>
</div>
<!-- TWO GIANT HELTER-SKELTER ANSWER BUTTONS -->
<div class="answer-chutes-grid">
<button class="ans-chute-btn noun" id="btnNoun" type="button">
<span class="key-hint-pill">⬅️ Arrow / 1 / N</span>
<div class="ac-slide-badge">🧱<span>🛝</span></div>
<div class="ac-text">
<div class="ac-title">NOUN</div>
<div class="ac-desc">Person, Animal, Place, or Thing</div>
<span class="ac-examples">🐱 cat • 🍎 apple • 🚌 bus • 👶 baby</span>
</div>
</button>
<button class="ans-chute-btn verb" id="btnVerb" type="button">
<span class="key-hint-pill">➡️ Arrow / 2 / V</span>
<div class="ac-text" style="text-align:right">
<div class="ac-title">VERB</div>
<div class="ac-desc">Action! Something You DO!</div>
<span class="ac-examples">🏃 run • 🤸 jump • 🍽️ eat • 👏 clap</span>
</div>
<div class="ac-slide-badge">🏃<span>🛝</span></div>
</button>
</div>
</main>
<!-- 4. QUICK-START / SETUP OVERLAY -->
<div class="overlay-screen open" id="setupOverlay" role="dialog" aria-modal="true">
<div class="dialog-card">
<div style="font-size:58px;line-height:1">🎢📦</div>
<h1 style="font-size:34px;font-weight:900;color:#1E3A8A;margin:6px 0 4px">Belter Noun or Verb Skelter!</h1>
<p style="font-size:15px;font-weight:800;color:#475569;margin:0 0 12px">
Easy Kindergarten words roll out on the conveyor belt! Press <b>🧱 NOUN</b> or <b>🏃 VERB</b> before the crate falls off the left edge!
</p>
<div class="setup-grid">
<div class="setup-box">
<div class="sb-title">👥 How Many Teams?</div>
<div class="sb-opts" id="optTeams">
<button type="button" class="sb-opt" data-teams="2">2 Teams</button>
<button type="button" class="sb-opt active" data-teams="3">3 Teams</button>
<button type="button" class="sb-opt" data-teams="4">4 Teams</button>
</div>
</div>
<div class="setup-box">
<div class="sb-title">⏱️ Conveyor Belt Speed</div>
<div class="sb-opts" id="optSpeed">
<button type="button" class="sb-opt" data-speedms="10000">🐢 10s</button>
<button type="button" class="sb-opt active" data-speedms="7000">🚶 7s</button>
<button type="button" class="sb-opt" data-speedms="5000">🚀 5s</button>
</div>
</div>
<div class="setup-box">
<div class="sb-title">📦 Crates per Round</div>
<div class="sb-opts" id="optRoundLen">
<button type="button" class="sb-opt" data-rlen="12">12</button>
<button type="button" class="sb-opt active" data-rlen="18">18</button>
<button type="button" class="sb-opt" data-rlen="24">24</button>
</div>
</div>
</div>
<button class="big-start-btn" id="startBeltBtn" type="button">
<span>▶</span> <span>START THE BELT!</span>
</button>
</div>
</div>
<!-- 5. END-OF-ROUND PODIUM & REVIEW OVERLAY -->
<div class="overlay-screen" id="podiumOverlay" role="dialog" aria-modal="true">
<div class="dialog-card">
<div style="font-size:56px;line-height:1">🏆</div>
<h2 style="font-size:32px;font-weight:900;color:#92400E;margin:4px 0" id="podiumWinnerTitle">Team Fox Leads!</h2>
<div style="font-size:14px;font-weight:800;color:#475569" id="podiumSubTitle">Round 1 Complete!</div>
<div class="podium-list" id="podiumList"></div>
<div id="missedSection" style="display:none;background:#FFF7ED;border:2.5px solid #FDBA74;border-radius:16px;padding:8px 12px;margin-bottom:12px">
<div style="font-size:13px;font-weight:900;color:#9A3412">🔎 Worth Another Look (Tap any word to hear it!):</div>
<div class="missed-chips" id="missedChips"></div>
</div>
<div style="display:flex;justify-content:center;gap:12px">
<button class="big-start-btn" id="nextRoundBtn" type="button" style="font-size:20px;padding:10px 26px">
<span>▶</span> <span>Next Round!</span>
</button>
<button class="big-start-btn" id="backToSetupBtn" type="button" style="font-size:18px;padding:10px 22px;background:#E2E8F0;border-color:#64748B;color:#1E293B;box-shadow:0 6px 0 #475569">
<span>⚙️</span> <span>Settings</span>
</button>
</div>
</div>
</div>
<!-- CONFETTI LAYER -->
<div class="confetti-layer" id="confettiLayer"></div>
</div>
</div>
<script src="assets/audio_belter_skelter.js"></script>
<script>
(function() {
"use strict";
/* ============ 1. PROPORTIONAL 1280x720 CONTAINMENT ============ */
var BASE_W = 1280;
var BASE_H = 720;
function fitAppToScreen() {
var vw = window.innerWidth || document.documentElement.clientWidth || BASE_W;
var vh = window.innerHeight || document.documentElement.clientHeight || BASE_H;
var pad = (vw < 900 || vh < 600) ? 4 : 10;
var scale = Math.min((vw - pad * 2) / BASE_W, (vh - pad * 2) / BASE_H);
scale = Math.max(0.28, Math.min(scale, 1.35));
document.documentElement.style.setProperty("--app-scale", scale.toFixed(4));
}
window.addEventListener("resize", fitAppToScreen);
window.addEventListener("orientationchange", fitAppToScreen);
fitAppToScreen();
/* ============ 2. SIMPLIFIED KINDERGARTEN POOL (20 EASY NOUNS + 20 EASY VERBS) ============ */
var EASY_WORDS = [
// 20 Concrete Nouns
{ word: "cat", type: "noun", emoji: "🐱", sentence: "Look at the fluffy cat!" },
{ word: "dog", type: "noun", emoji: "🐶", sentence: "The happy dog wags its tail!" },
{ word: "pig", type: "noun", emoji: "🐷", sentence: "The pink pig loves mud!" },
{ word: "cow", type: "noun", emoji: "🐮", sentence: "The cow gives us fresh milk!" },
{ word: "hen", type: "noun", emoji: "🐔", sentence: "The farm hen laid an egg!" },
{ word: "frog", type: "noun", emoji: "🐸", sentence: "The green frog sits on a pad!" },
{ word: "lion", type: "noun", emoji: "🦁", sentence: "The lion has a golden mane!" },
{ word: "bear", type: "noun", emoji: "🐻", sentence: "The brown bear lives in the woods!" },
{ word: "apple", type: "noun", emoji: "🍎", sentence: "An apple is a crunchy red fruit!" },
{ word: "cake", type: "noun", emoji: "🎂", sentence: "We eat sweet birthday cake!" },
{ word: "egg", type: "noun", emoji: "🥚", sentence: "I eat a warm egg for breakfast!" },
{ word: "ball", type: "noun", emoji: "⚽", sentence: "Bounce and kick the round ball!" },
{ word: "book", type: "noun", emoji: "📖", sentence: "Open the book to read a story!" },
{ word: "hat", type: "noun", emoji: "👒", sentence: "Put the sunny hat on your head!" },
{ word: "bed", type: "noun", emoji: "🛏️", sentence: "Rest your head on the cozy bed!" },
{ word: "cup", type: "noun", emoji: "🥤", sentence: "Pour cool water into the cup!" },
{ word: "bus", type: "noun", emoji: "🚌", sentence: "The yellow bus goes to school!" },
{ word: "car", type: "noun", emoji: "🚗", sentence: "The red car drives on the road!" },
{ word: "park", type: "noun", emoji: "🏞️", sentence: "We play on slides at the park!" },
{ word: "baby", type: "noun", emoji: "👶", sentence: "The little baby smiles and giggles!" },
// 20 Action Verbs
{ word: "run", type: "verb", emoji: "🏃", sentence: "I can run fast with my legs!" },
{ word: "jump", type: "verb", emoji: "🤸", sentence: "I can jump high in the air!" },
{ word: "hop", type: "verb", emoji: "🦘", sentence: "Hop on one foot like a bunny!" },
{ word: "walk", type: "verb", emoji: "🚶", sentence: "Walk nicely down the path!" },
{ word: "swim", type: "verb", emoji: "🏊", sentence: "Swim like a fish in the pool!" },
{ word: "fly", type: "verb", emoji: "🦅", sentence: "Birds can fly high in the sky!" },
{ word: "sit", type: "verb", emoji: "🪑", sentence: "Please sit down on the chair!" },
{ word: "climb", type: "verb", emoji: "🧗", sentence: "Climb up the tall green tree!" },
{ word: "eat", type: "verb", emoji: "🍽️", sentence: "I eat my yummy lunch!" },
{ word: "drink", type: "verb", emoji: "🧃", sentence: "Drink cold orange juice!" },
{ word: "sleep", type: "verb", emoji: "😴", sentence: "Close your eyes and sleep!" },
{ word: "wash", type: "verb", emoji: "🧼", sentence: "Wash your hands with bubbly soap!" },
{ word: "read", type: "verb", emoji: "📚", sentence: "I read a funny storybook!" },
{ word: "draw", type: "verb", emoji: "🖍️", sentence: "Draw a rainbow with crayons!" },
{ word: "clap", type: "verb", emoji: "👏", sentence: "Clap your hands together!" },
{ word: "kick", type: "verb", emoji: "🦵", sentence: "Kick the ball into the goal!" },
{ word: "sing", type: "verb", emoji: "🎤", sentence: "Sing a happy song out loud!" },
{ word: "cry", type: "verb", emoji: "😢", sentence: "Babies cry when they are hungry!" },
{ word: "hug", type: "verb", emoji: "🤗", sentence: "Give your friend a big warm hug!" },
{ word: "dig", type: "verb", emoji: "", sentence: "Dig a hole in the soft sand!" }
];
var TEAM_PALETTES = [
{ name: "Team Fox", icon: "🦊", c: "#EA580C", bg: "#FFEDD5", bd: "#FB923C" },
{ name: "Team Panda", icon: "🐼", c: "#2563EB", bg: "#DBEAFE", bd: "#60A5FA" },
{ name: "Team Lion", icon: "🦁", c: "#D97706", bg: "#FEF3C7", bd: "#FBBF24" },
{ name: "Team Frog", icon: "🐸", c: "#059669", bg: "#D1FAE5", bd: "#34D399" }
];
function shuffle(arr) {
var a = arr.slice();
for (var i = a.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = a[i]; a[i] = a[j]; a[j] = tmp;
}
return a;
}
/* ============ 3. GAME STATE ============ */
var state = {
teamCount: 3,
travelMs: 7000,
roundLen: 18,
roundNum: 1,
teams: [],
deck: [],
turnIdx: 0,
nounSorted: 0,
verbSorted: 0,
missedWords: [],
armed: false,
paused: false,
rideStartedAt: 0,
remainingMs: 7000,
pausedAt: 0,
turnTimeoutId: null,
nextTurnTimerId: null,
readyTimerId: null,
soundOn: true,
audioCtx: null,
decoded: {},
activeSources: [],
activeHtmlAudio: null
};
function initTeams() {
state.teams = [];
for (var i = 0; i < state.teamCount; i++) {
var p = TEAM_PALETTES[i];
state.teams.push({
idx: i,
name: p.name,
icon: p.icon,
c: p.c,
bg: p.bg,
bd: p.bd,
score: 0,
right: 0,
wrong: 0,
streak: 0,
bestStreak: 0
});
}
}
function buildBalancedDeck(count) {
var nouns = shuffle(EASY_WORDS.filter(function(w) { return w.type === "noun"; }));
var verbs = shuffle(EASY_WORDS.filter(function(w) { return w.type === "verb"; }));
var combined = [];
for (var i = 0; i < Math.ceil(count / 2); i++) {
combined.push(nouns[i % nouns.length]);
combined.push(verbs[i % verbs.length]);
}
return shuffle(combined.slice(0, count));
}
/* ============ 4. STUDIO VOICE ENGINE (US FEMALE JENNY + UK MALE RYAN) ============ */
function ensureAudio() {
if (!state.audioCtx) {
var AC = window.AudioContext || window.webkitAudioContext;
if (AC) state.audioCtx = new AC();
}
if (state.audioCtx && state.audioCtx.state === "suspended") {
state.audioCtx.resume().catch(function() {});
}
return state.audioCtx;
}
function stopAllSpeech() {
state.activeSources.forEach(function(s) {
try { s.stop(0); } catch (e) {}
});
state.activeSources = [];
if (state.activeHtmlAudio) {
try {
state.activeHtmlAudio.pause();
state.activeHtmlAudio.currentTime = 0;
} catch (e) {}
state.activeHtmlAudio = null;
}
}
function playSfx(type) {
if (!state.soundOn) return;
var ctx = ensureAudio();
if (!ctx) return;
try {
var now = ctx.currentTime;
if (type === "right") {
[523.25, 659.25, 783.99, 1046.5].forEach(function(f, idx) {
var o = ctx.createOscillator();
var g = ctx.createGain();
o.type = "triangle";
o.frequency.setValueAtTime(f, now + idx * 0.06);
g.gain.setValueAtTime(0.001, now + idx * 0.06);
g.gain.exponentialRampToValueAtTime(0.11, now + idx * 0.06 + 0.015);
g.gain.exponentialRampToValueAtTime(0.0001, now + idx * 0.06 + 0.22);
o.connect(g); g.connect(ctx.destination);
o.start(now + idx * 0.06);
o.stop(now + idx * 0.06 + 0.24);
});
} else if (type === "wrong") {
var o = ctx.createOscillator();
var g = ctx.createGain();
o.type = "sawtooth";
o.frequency.setValueAtTime(220, now);
o.frequency.exponentialRampToValueAtTime(115, now + 0.28);
g.gain.setValueAtTime(0.001, now);
g.gain.exponentialRampToValueAtTime(0.11, now + 0.02);
g.gain.exponentialRampToValueAtTime(0.0001, now + 0.3);
o.connect(g); g.connect(ctx.destination);
o.start(now); o.stop(now + 0.32);
} else {
var o2 = ctx.createOscillator();
var g2 = ctx.createGain();
o2.type = "sine";
o2.frequency.setValueAtTime(480, now);
o2.frequency.exponentialRampToValueAtTime(680, now + 0.1);
g2.gain.setValueAtTime(0.08, now);
g2.gain.exponentialRampToValueAtTime(0.0001, now + 0.12);
o2.connect(g2); g2.connect(ctx.destination);
o2.start(now); o2.stop(now + 0.13);
}
} catch (e) {}
}
function computeTrimSpan(buf) {
var ch = buf.getChannelData(0);
var sr = buf.sampleRate;
var n = ch.length;
var thr = 0.012;
var first = 0;
var last = n - 1;
for (var i = 0; i < n; i++) {
if (Math.abs(ch[i]) > thr) { first = i; break; }
}
for (var j = n - 1; j >= 0; j--) {
if (Math.abs(ch[j]) > thr) { last = j; break; }
}
var start = Math.max(0, (first / sr) - 0.02);
var end = Math.min(buf.duration, (last / sr) + 0.045);
return [start, Math.max(start + 0.08, end)];
}
async function decodeClip(key) {
if (state.decoded[key]) return state.decoded[key];
var meta = window.BELTER_SKELTER_AUDIO && window.BELTER_SKELTER_AUDIO[key];
if (!meta || !meta.src) return null;
var ctx = ensureAudio();
if (!ctx) return null;
var b64 = meta.src.split(",")[1];
var bin = atob(b64);
var bytes = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
var buf = await ctx.decodeAudioData(bytes.buffer.slice(0));
buf._trimSpan = computeTrimSpan(buf);
state.decoded[key] = buf;
return buf;
}
async function playClip(key) {
if (!state.soundOn) {
await new Promise(function(r) { setTimeout(r, 900); });
return;
}
var meta = window.BELTER_SKELTER_AUDIO && window.BELTER_SKELTER_AUDIO[key];
if (!meta) return;
stopAllSpeech();
var ctx = ensureAudio();
if (ctx) {
try {
var buf = await decodeClip(key);
if (buf) {
var span = buf._trimSpan || [0, buf.duration];
var start = span[0];
var dur = Math.max(0.08, span[1] - start);
await new Promise(function(resolve) {
var settled = false;
function done() {
if (settled) return;
settled = true;
resolve();
}
var src = ctx.createBufferSource();
src.buffer = buf;
src.connect(ctx.destination);
src.onended = done;
src.start(ctx.currentTime + 0.01, start, dur);
state.activeSources.push(src);
setTimeout(done, Math.ceil(dur * 1000) + 120);
});
return;
}
} catch (e) {}
}
await new Promise(function(resolve) {
var a = new Audio(meta.src);
state.activeHtmlAudio = a;
a.onended = resolve;
a.onerror = resolve;
a.play().catch(resolve);
});
}
function triggerConfetti() {
var layer = document.getElementById("confettiLayer");
if (!layer) return;
var colors = ["#F59E0B", "#2563EB", "#EA580C", "#10B981", "#EC4899", "#FACC15"];
for (var i = 0; i < 34; i++) {
var dot = document.createElement("div");
dot.className = "confetti-dot";
var angle = (Math.PI * 2 * i) / 34 + (Math.random() * 0.25);
var dist = 90 + Math.random() * 230;
dot.style.left = "50%";
dot.style.top = "45%";
dot.style.background = colors[i % colors.length];
dot.style.setProperty("--tx", Math.cos(angle) * dist + "px");
dot.style.setProperty("--ty", Math.sin(angle) * dist + "px");
layer.appendChild(dot);
setTimeout(function(el) {
if (el && el.parentNode) el.parentNode.removeChild(el);
}, 950, dot);
}
}
/* ============ 5. RENDER TEAM SCOREBOARD & HUD ============ */
function renderScoreboard() {
var row = document.getElementById("teamCardsRow");
if (!row) return;
row.style.setProperty("--team-count", String(state.teams.length));
var activeTeamIdx = state.turnIdx % state.teams.length;
row.innerHTML = state.teams.map(function(t, idx) {
var isTurn = idx === activeTeamIdx;
return (
'<div class="team-score-card' + (isTurn ? ' current-turn' : '') + '" id="teamCard_' + idx + '" style="border-color:' + (isTurn ? t.c : t.bd) + '">' +
'<div class="tsc-left">' +
'<div class="tsc-avatar" style="background:' + t.c + '">' + t.icon + '</div>' +
'<div>' +
'<div class="tsc-name">' + t.name + '</div>' +
'<div class="tsc-sub">✅ ' + t.right + ' • 🔥 Streak ' + t.streak + '</div>' +
'</div>' +
'</div>' +
'<div class="tsc-right">' +
(isTurn ? '<span class="tsc-turn-pill">UP NOW</span>' : '') +
'<div class="tsc-pts" style="color:' + t.c + '">' + t.score + '</div>' +
'</div>' +
'</div>'
);
}).join("");
document.getElementById("roundLabelTxt").textContent =
"Round " + state.roundNum + " • Crate " + Math.min(state.turnIdx + 1, state.deck.length) + " / " + state.deck.length;
document.getElementById("nounBinCount").textContent = String(state.nounSorted);
document.getElementById("verbBinCount").textContent = String(state.verbSorted);
}
function clearTimers() {
state.turnToken = (state.turnToken || 0) + 1;
if (state.turnTimeoutId) { clearTimeout(state.turnTimeoutId); state.turnTimeoutId = null; }
if (state.nextTurnTimerId) { clearTimeout(state.nextTurnTimerId); state.nextTurnTimerId = null; }
if (state.readyTimerId) { clearTimeout(state.readyTimerId); state.readyTimerId = null; }
}
function setBeltMotionPaused(isPaused) {
var rider = document.getElementById("crateRider");
var treads = document.getElementById("conveyorTreads");
var bar = document.getElementById("timerBarFill");
if (rider) rider.classList.toggle("paused", isPaused);
if (treads) treads.classList.toggle("paused", isPaused);
if (bar) bar.classList.toggle("paused", isPaused);
document.querySelectorAll(".roller-wheel").forEach(function(w) {
w.classList.toggle("paused", isPaused);
});
}
/* ============ 6. CORE CONVEYOR BELT TURN ENGINE ============ */
function startNewMatch() {
clearTimers();
state.roundNum = 1;
state.nounSorted = 0;
state.verbSorted = 0;
state.missedWords = [];
initTeams();
startRound();
}
function startRound() {
clearTimers();
state.deck = buildBalancedDeck(state.roundLen);
state.turnIdx = 0;
state.paused = false;
document.getElementById("pauseBeltBtn").classList.remove("pause-active");
document.getElementById("pauseBeltBtn").innerHTML = "⏸️ Pause Belt";
document.getElementById("setupOverlay").classList.remove("open");
document.getElementById("podiumOverlay").classList.remove("open");
document.documentElement.style.setProperty("--ride-ms", state.travelMs + "ms");
renderScoreboard();
runTurn();
}
async function runTurn() {
clearTimers();
var myToken = state.turnToken;
if (state.turnIdx >= state.deck.length) {
showPodiumScreen();
return;
}
var item = state.deck[state.turnIdx];
var team = state.teams[state.turnIdx % state.teams.length];
state.armed = false;
renderScoreboard();
// Update Turn Banner
var turnPill = document.getElementById("turnBannerPill");
turnPill.innerHTML = '<span>' + team.icon + '</span> <span>' + team.name + '’s Turn!</span>';
turnPill.style.borderColor = team.bd;
// Hide previous reveal & stamps
var revealCard = document.getElementById("revealCard");
revealCard.classList.remove("show");
var crate = document.getElementById("wordCrate");
crate.className = "word-crate";
crate.querySelectorAll(".verdict-stamp, .floating-score").forEach(function(el) { el.remove(); });
document.getElementById("cratePic").textContent = item.emoji;
document.getElementById("crateWordTxt").textContent = item.word;
document.getElementById("crateQBadge").textContent = "?";
var btnNoun = document.getElementById("btnNoun");
var btnVerb = document.getElementById("btnVerb");
btnNoun.className = "ans-chute-btn noun";
btnVerb.className = "ans-chute-btn verb";
btnNoun.disabled = true;
btnVerb.disabled = true;
// Reset rider position off-screen while showing quick "Get Ready!" card
var rider = document.getElementById("crateRider");
rider.className = "crate-rider";
rider.style.display = "none";
var bar = document.getElementById("timerBarFill");
bar.className = "timer-bar-fill";
var grCard = document.getElementById("getReadyCard");
document.getElementById("grIcon").textContent = team.icon;
document.getElementById("grTitle").textContent = team.name + " — Get Ready!";
grCard.style.display = "block";
await playClip("turn_" + team.idx);
if (myToken !== state.turnToken) return;
state.readyTimerId = setTimeout(function() {
if (myToken !== state.turnToken) return;
grCard.style.display = "none";
launchCrateOnBelt(item);
}, 220);
}
function launchCrateOnBelt(item) {
var rider = document.getElementById("crateRider");
var bar = document.getElementById("timerBarFill");
var btnNoun = document.getElementById("btnNoun");
var btnVerb = document.getElementById("btnVerb");
document.documentElement.style.setProperty("--ride-ms", state.travelMs + "ms");
rider.style.display = "block";
rider.className = "crate-rider";
void rider.offsetWidth; // Reflow to restart animation cleanly
rider.className = "crate-rider riding";
bar.className = "timer-bar-fill";
void bar.offsetWidth;
bar.className = "timer-bar-fill draining";
setBeltMotionPaused(false);
btnNoun.disabled = false;
btnVerb.disabled = false;
state.armed = true;
state.paused = false;
state.rideStartedAt = performance.now();
state.remainingMs = state.travelMs;
// Speak the word immediately as the crate rolls onto the belt!
playSfx("pop");
playClip("say_" + item.word);
state.turnTimeoutId = setTimeout(function() {
resolveTurn(null);
}, state.travelMs);
}
async function resolveTurn(pressedType) {
if (!state.armed) return;
state.armed = false;
clearTimers();
var myToken = state.turnToken;
var elapsed = performance.now() - state.rideStartedAt;
var limit = state.travelMs;
// Freeze the conveyor belt and crate right where it was caught!
setBeltMotionPaused(true);
var item = state.deck[state.turnIdx];
var team = state.teams[state.turnIdx % state.teams.length];
var timedOut = (pressedType === null);
var correct = !timedOut && (pressedType === item.type);
var btnNoun = document.getElementById("btnNoun");
var btnVerb = document.getElementById("btnVerb");
btnNoun.disabled = true;
btnVerb.disabled = true;
if (pressedType === "noun") btnNoun.classList.add(correct ? "hit-right" : "hit-wrong");
if (pressedType === "verb") btnVerb.classList.add(correct ? "hit-right" : "hit-wrong");
var pts = 0;
var streakBonus = 0;
if (correct) {
var ratioLeft = Math.max(0, Math.min(1, 1 - (elapsed / limit)));
pts = Math.round((40 + 60 * ratioLeft) / 5) * 5;
team.streak += 1;
if (team.streak > team.bestStreak) team.bestStreak = team.streak;
if (team.streak >= 2) streakBonus = 15;
if (team.streak >= 4) streakBonus = 25;
team.right += 1;
team.score += (pts + streakBonus);
if (item.type === "noun") state.nounSorted += 1;
else state.verbSorted += 1;
} else {
team.streak = 0;
team.wrong += 1;
state.missedWords.push(item);
}
// Stamp the crate & show floating points
var crate = document.getElementById("wordCrate");
crate.classList.add(correct ? ("right-" + item.type) : "wrong-hit");
var stamp = document.createElement("div");
stamp.className = "verdict-stamp";
var stampTxt = timedOut ? "⏰ MISSED!" : (correct ? ("✓ " + item.type.toUpperCase() + "!") : "✕ OOPS!");
stamp.innerHTML = '<div class="stamp-pill ' + (correct ? 'ok' : 'no') + '">' + stampTxt + '</div>';
crate.appendChild(stamp);
var floatEl = document.createElement("div");
floatEl.className = "floating-score" + (correct ? "" : " zero");
floatEl.textContent = correct
? ("+" + (pts + streakBonus) + (streakBonus ? " 🔥" : ""))
: "+0";
crate.appendChild(floatEl);
if (correct) {
playSfx("right");
triggerConfetti();
var cardEl = document.getElementById("teamCard_" + team.idx);
if (cardEl) cardEl.classList.add("bump");
} else {
playSfx("wrong");
var mf = document.getElementById("machineFrame");
mf.classList.add("shake");
setTimeout(function() { mf.classList.remove("shake"); }, 480);
}
// Show Worked Sentence Reveal Card & Speak British Male Ryan's Reveal!
var revealCard = document.getElementById("revealCard");
var revealBadge = document.getElementById("revealBadge");
var revealSentence = document.getElementById("revealSentence");
revealBadge.className = "rv-badge " + item.type;
revealBadge.textContent = item.type === "noun" ? "🧱 IT'S A NOUN!" : "🏃 IT'S A VERB!";
revealSentence.textContent = item.emoji + " " + item.sentence;
revealCard.classList.add("show");
renderScoreboard();
// Wait for British Male Ryan to completely finish reading the answer & sentence!
if (timedOut) {
await playClip("time_up");
if (myToken !== state.turnToken) return;
await playClip("ans_" + item.word);
} else {
await playClip("ans_" + item.word);
}
if (myToken !== state.turnToken) return;
// Give a comfortable 750ms breathing pause AFTER Ryan finishes speaking before advancing!
state.nextTurnTimerId = setTimeout(function() {
if (myToken !== state.turnToken) return;
state.turnIdx += 1;
runTurn();
}, 750);
}
/* ============ 7. PAUSE / RESUME & SWAP WORD ============ */
function togglePauseBelt() {
var pauseBtn = document.getElementById("pauseBeltBtn");
if (!state.armed) return;
if (!state.paused) {
state.paused = true;
clearTimeout(state.turnTimeoutId);
var spent = performance.now() - state.rideStartedAt;
state.remainingMs = Math.max(450, state.remainingMs - spent);
setBeltMotionPaused(true);
pauseBtn.classList.add("pause-active");
pauseBtn.innerHTML = "▶ Resume Belt";
} else {
state.paused = false;
state.rideStartedAt = performance.now();
setBeltMotionPaused(false);
pauseBtn.classList.remove("pause-active");
pauseBtn.innerHTML = "⏸️ Pause Belt";
state.turnTimeoutId = setTimeout(function() {
resolveTurn(null);
}, state.remainingMs);
}
}
function swapCurrentWord() {
clearTimers();
var cur = state.deck[state.turnIdx];
var pool = EASY_WORDS.filter(function(w) { return !cur || w.word !== cur.word; });
state.deck[state.turnIdx] = pool[Math.floor(Math.random() * pool.length)];
runTurn();
}
/* ============ 8. END-OF-ROUND PODIUM & REVIEW SCREEN ============ */
function showPodiumScreen() {
clearTimers();
setBeltMotionPaused(true);
var ranked = state.teams.slice().sort(function(a, b) { return b.score - a.score; });
var medals = ["🥇", "🥈", "🥉", "🏅"];
document.getElementById("podiumWinnerTitle").textContent =
ranked[0].icon + " " + ranked[0].name + " Wins Round " + state.roundNum + "!";
document.getElementById("podiumSubTitle").textContent =
"🧱 " + state.nounSorted + " Nouns & 🏃 " + state.verbSorted + " Verbs Sorted!";
var listEl = document.getElementById("podiumList");
listEl.innerHTML = ranked.map(function(t, i) {
return (
'<div class="podium-row" style="border-color:' + t.bd + '">' +
'<div style="display:flex;align-items:center;gap:10px">' +
'<span style="font-size:28px">' + medals[i] + '</span>' +
'<span style="font-size:26px">' + t.icon + '</span>' +
'<div>' +
'<div style="font-size:17px;font-weight:900;color:#0F172A">' + t.name + '</div>' +
'<div style="font-size:12px;font-weight:800;color:#475569">✅ ' + t.right + ' right • Best streak 🔥 ' + t.bestStreak + '</div>' +
'</div>' +
'</div>' +
'<div style="font-size:26px;font-weight:900;color:' + t.c + '">' + t.score + ' pts</div>' +
'</div>'
);
}).join("");
// Missed words review chips
var missedSec = document.getElementById("missedSection");
var missedChips = document.getElementById("missedChips");
if (state.missedWords.length > 0) {
var seen = {};
var uniq = state.missedWords.filter(function(m) {
if (seen[m.word]) return false;
seen[m.word] = true;
return true;
});
missedSec.style.display = "block";
missedChips.innerHTML = uniq.map(function(m) {
return (
'<button type="button" class="missed-chip" data-review="' + m.word + '">' +
'<span>' + m.emoji + '</span> <span>' + m.word + '</span> ' +
'<span style="color:' + (m.type === "noun" ? "#2563EB" : "#EA580C") + '">(' + m.type.toUpperCase() + ')</span>' +
'</button>'
);
}).join("");
missedChips.querySelectorAll("[data-review]").forEach(function(btn) {
btn.addEventListener("click", function() {
playClip("ans_" + btn.getAttribute("data-review"));
});
});
} else {
missedSec.style.display = "none";
}
document.getElementById("podiumOverlay").classList.add("open");
triggerConfetti();
playClip("winner");
}
/* ============ 9. WIRE BUTTONS & KEYBOARD SHORTCUTS ============ */
document.getElementById("btnNoun").addEventListener("click", function() {
resolveTurn("noun");
});
document.getElementById("btnVerb").addEventListener("click", function() {
resolveTurn("verb");
});
document.getElementById("wordCrate").addEventListener("click", function() {
var item = state.deck[state.turnIdx];
if (item) playClip("say_" + item.word);
});
document.getElementById("repeatBtn").addEventListener("click", function() {
var item = state.deck[state.turnIdx];
if (item) playClip(state.armed ? ("say_" + item.word) : ("ans_" + item.word));
});
document.getElementById("pauseBeltBtn").addEventListener("click", togglePauseBelt);
document.getElementById("swapWordBtn").addEventListener("click", swapCurrentWord);
document.getElementById("finishRoundBtn").addEventListener("click", showPodiumScreen);
document.getElementById("ruleNounChip").addEventListener("click", function() {
playClip("rule_noun");
});
document.getElementById("ruleVerbChip").addEventListener("click", function() {
playClip("rule_verb");
});
// Top HUD Speed Switcher
document.querySelectorAll(".speed-btn").forEach(function(btn) {
btn.addEventListener("click", function() {
state.travelMs = parseInt(btn.getAttribute("data-ms"), 10);
document.documentElement.style.setProperty("--ride-ms", state.travelMs + "ms");
document.querySelectorAll(".speed-btn").forEach(function(b) {
b.classList.toggle("active", b === btn);
});
});
});
// Setup Dialog Options
document.querySelectorAll("[data-teams]").forEach(function(btn) {
btn.addEventListener("click", function() {
state.teamCount = parseInt(btn.getAttribute("data-teams"), 10);
document.querySelectorAll("[data-teams]").forEach(function(b) {
b.classList.toggle("active", b === btn);
});
});
});
document.querySelectorAll("[data-speedms]").forEach(function(btn) {
btn.addEventListener("click", function() {
state.travelMs = parseInt(btn.getAttribute("data-speedms"), 10);
document.querySelectorAll("[data-speedms]").forEach(function(b) {
b.classList.toggle("active", b === btn);
});
document.querySelectorAll(".speed-btn").forEach(function(b) {
b.classList.toggle("active", parseInt(b.getAttribute("data-ms"), 10) === state.travelMs);
});
});
});
document.querySelectorAll("[data-rlen]").forEach(function(btn) {
btn.addEventListener("click", function() {
state.roundLen = parseInt(btn.getAttribute("data-rlen"), 10);
document.querySelectorAll("[data-rlen]").forEach(function(b) {
b.classList.toggle("active", b === btn);
});
});
});
document.getElementById("startBeltBtn").addEventListener("click", function() {
ensureAudio();
startNewMatch();
});
document.getElementById("nextRoundBtn").addEventListener("click", function() {
state.roundNum += 1;
state.missedWords = [];
startRound();
});
document.getElementById("setupBtn").addEventListener("click", function() {
clearTimers();
setBeltMotionPaused(true);
document.getElementById("setupOverlay").classList.add("open");
});
document.getElementById("backToSetupBtn").addEventListener("click", function() {
document.getElementById("podiumOverlay").classList.remove("open");
document.getElementById("setupOverlay").classList.add("open");
});
document.getElementById("soundBtn").addEventListener("click", function() {
state.soundOn = !state.soundOn;
if (!state.soundOn) stopAllSpeech();
document.getElementById("soundIcon").textContent = state.soundOn ? "🔊" : "🔇";
});
document.getElementById("fullBtn").addEventListener("click", function() {
var el = document.documentElement;
if (!document.fullscreenElement) {
if (el.requestFullscreen) el.requestFullscreen().catch(function() {});
} else {
if (document.exitFullscreen) document.exitFullscreen().catch(function() {});
}
});
window.addEventListener("keydown", function(e) {
var k = e.key.toLowerCase();
if (k === "arrowleft" || k === "1" || k === "n") {
e.preventDefault();
resolveTurn("noun");
} else if (k === "arrowright" || k === "2" || k === "v") {
e.preventDefault();
resolveTurn("verb");
} else if (k === "p") {
e.preventDefault();
togglePauseBelt();
}
});
// Initialize initial preview state behind Setup Dialog
initTeams();
state.deck = buildBalancedDeck(state.roundLen);
renderScoreboard();
})();
</script>
</body>
</html>