Исходники пакета · Belter Noun or Verb Skelter
CC BY 4.0 · укажите @joe · v1
Берите, делайте ремикс, учите с ним
Код ниже — только играемый входной HTML пакета. Скачайте полный ZIP, чтобы править или ремикшировать игру со всеми относительными ресурсами.
Заготовка подсказки для ИИ
Edit this classroom game for interactive whiteboards. Keep large touch targets and whole-class play. Tracking, logins, personal-data collection, and remote API calls are prohibited. Use the downloaded complete ZIP—not only the entry HTML shown below—and preserve its browser-ready folder structure. Preserve the educational goal. Original creator: @joe on 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>