套件原始碼 · Am, Is, Are All-Stars
CC BY 4.0 · 請標明 @joe · v1
AI 編輯提示起頭
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 · 4555.2 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>am, is, are — Verb to Be Kingdom & Cup Castle Race</title>
<style>
:root {
--app-scale: 1;
--ink: #1E293B;
--am-c: #EC4899;
--am-bg: #FDF2F8;
--am-bd: #F472B6;
--is-c: #2563EB;
--is-bg: #EFF6FF;
--is-bd: #60A5FA;
--are-c: #059669;
--are-bg: #ECFDF5;
--are-bd: #34D399;
--gold: #F59E0B;
}
* {
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 20% 15%, #38BDF8 0%, #2563EB 48%, #1E1B4B 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, #F0F9FF 0%, #ECFDF5 55%, #FEFCE8 100%);
border-radius: 30px;
border: 6px solid #FBBF24;
box-shadow: 0 24px 65px rgba(15, 23, 42, 0.55), inset 0 2px 0 rgba(255,255,255,0.9);
display: flex;
flex-direction: column;
padding: 12px 16px;
gap: 9px;
position: relative;
overflow: hidden;
}
/* Soft decorative background dots */
.stage-bg-pattern {
position: absolute;
inset: 0;
pointer-events: none;
background-image: radial-gradient(rgba(59, 130, 246, 0.12) 2px, transparent 2px);
background-size: 32px 32px;
z-index: 0;
}
/* ============ 1. TOP HEADER BAR ============ */
.top-bar {
position: relative;
z-index: 5;
height: 60px;
background: linear-gradient(90deg, #1E3A8A 0%, #2563EB 50%, #0D9488 100%);
border-radius: 20px;
border: 3px solid #93C5FD;
box-shadow: 0 6px 0 rgba(30, 58, 138, 0.35);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 14px;
color: #fff;
flex-shrink: 0;
}
.brand-cluster {
display: flex;
align-items: center;
gap: 10px;
}
.brand-logo {
width: 44px;
height: 44px;
border-radius: 14px;
background: linear-gradient(135deg, #FDE047, #F97316);
border: 3px solid #FFFBEB;
display: flex;
align-items: center;
justify-content: center;
font-size: 25px;
box-shadow: 0 4px 0 rgba(124, 45, 18, 0.35);
}
.brand-title {
font-size: 24px;
font-weight: 900;
letter-spacing: 0.4px;
line-height: 1.05;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.35);
}
.brand-sub {
font-size: 11.5px;
font-weight: 800;
color: #BAE6FD;
letter-spacing: 0.3px;
}
/* Mode Switch Tabs */
.mode-tabs {
display: flex;
align-items: center;
gap: 6px;
background: rgba(15, 23, 42, 0.28);
padding: 4px;
border-radius: 14px;
border: 2px solid rgba(255, 255, 255, 0.25);
}
.mode-tab {
border: none;
background: transparent;
color: #E0F2FE;
font-family: inherit;
font-size: 13.5px;
font-weight: 900;
padding: 7px 13px;
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.16s ease;
}
.mode-tab:hover {
background: rgba(255, 255, 255, 0.14);
color: #fff;
}
.mode-tab.active {
background: linear-gradient(180deg, #FDE047, #F59E0B);
color: #451A03;
box-shadow: 0 3px 0 #B45309;
transform: translateY(-1px);
}
/* Right utility controls (strictly 4 clean buttons + star counter) */
.top-right {
display: flex;
align-items: center;
gap: 7px;
}
.star-pill {
background: rgba(255, 255, 255, 0.18);
border: 2px solid #FDE047;
border-radius: 999px;
padding: 5px 11px;
font-size: 14px;
font-weight: 900;
color: #FEF08A;
display: flex;
align-items: center;
gap: 5px;
}
.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;
transition: transform 0.12s, background 0.15s;
}
.util-btn:hover {
background: rgba(255, 255, 255, 0.28);
transform: translateY(-1px);
}
.util-btn:active {
transform: translateY(1px);
}
/* ============ 2. PRONOUN GRAMMAR HELPER & VOICE BAR ============ */
.grammar-bar {
position: relative;
z-index: 5;
height: 58px;
background: #FFFFFF;
border-radius: 18px;
border: 3px solid #BFDBFE;
box-shadow: 0 5px 0 rgba(59, 130, 246, 0.12);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
gap: 10px;
flex-shrink: 0;
}
.rule-pills {
display: flex;
align-items: center;
gap: 8px;
}
.rule-pill {
border: 3px solid transparent;
border-radius: 14px;
padding: 5px 11px;
cursor: pointer;
display: flex;
align-items: center;
gap: 7px;
font-family: inherit;
transition: transform 0.14s, box-shadow 0.14s;
}
.rule-pill:hover {
transform: translateY(-2px);
}
.rule-pill.am {
background: var(--am-bg);
border-color: var(--am-bd);
color: #9D174D;
}
.rule-pill.is {
background: var(--is-bg);
border-color: var(--is-bd);
color: #1E40AF;
}
.rule-pill.are {
background: var(--are-bg);
border-color: var(--are-bd);
color: #065F46;
}
.rule-pill.highlight {
transform: scale(1.05);
box-shadow: 0 0 0 4px rgba(251, 191, 36, 0.55);
}
.rp-verb {
font-size: 17px;
font-weight: 900;
padding: 2px 8px;
border-radius: 9px;
color: #fff;
text-transform: lowercase;
}
.rule-pill.am .rp-verb { background: var(--am-c); }
.rule-pill.is .rp-verb { background: var(--is-c); }
.rule-pill.are .rp-verb { background: var(--are-c); }
.rp-subj {
font-size: 12.5px;
font-weight: 900;
line-height: 1.15;
text-align: left;
}
.rp-subj small {
display: block;
font-size: 10.5px;
opacity: 0.85;
}
.voice-actions {
display: flex;
align-items: center;
gap: 7px;
}
.act-btn {
border: 3px solid #F59E0B;
background: linear-gradient(180deg, #FEF08A, #FBBF24);
color: #78350F;
font-family: inherit;
font-size: 13px;
font-weight: 900;
padding: 6px 12px;
border-radius: 13px;
box-shadow: 0 4px 0 #B45309;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
transition: transform 0.12s;
}
.act-btn:hover { transform: translateY(-1px); }
.act-btn:active { transform: translateY(2px); box-shadow: 0 1px 0 #B45309; }
.act-btn.uk {
border-color: #10B981;
background: linear-gradient(180deg, #A7F3D0, #34D399);
color: #064E3B;
box-shadow: 0 4px 0 #047857;
}
.act-btn.next {
border-color: #6366F1;
background: linear-gradient(180deg, #C7D2FE, #818CF8);
color: #1E1B4B;
box-shadow: 0 4px 0 #4338CA;
}
/* ============ 3. MAIN SPLIT: LEFT GRAMMAR STAGE + RIGHT CUP CASTLE RACE ============ */
.main-split {
position: relative;
z-index: 5;
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 1fr 375px;
gap: 12px;
}
.grammar-stage {
background: rgba(255, 255, 255, 0.92);
border-radius: 24px;
border: 4px solid #93C5FD;
box-shadow: 0 10px 25px rgba(30, 58, 138, 0.12);
padding: 12px 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
overflow: hidden;
}
/* World Theme Filter Pills */
.theme-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
flex-shrink: 0;
}
.theme-chips {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.theme-chip {
border: 2px solid #CBD5E1;
background: #F8FAFC;
color: #475569;
font-family: inherit;
font-size: 12px;
font-weight: 800;
padding: 4px 10px;
border-radius: 999px;
cursor: pointer;
transition: all 0.14s;
}
.theme-chip.active {
background: #2563EB;
border-color: #1D4ED8;
color: #fff;
box-shadow: 0 2px 0 #1E3A8A;
}
.q-counter-badge {
font-size: 12.5px;
font-weight: 900;
color: #1E40AF;
background: #DBEAFE;
border: 2px solid #93C5FD;
padding: 4px 11px;
border-radius: 999px;
}
/* Illustrated Scene & Subject Detective Card */
.scene-card {
background: linear-gradient(180deg, #F8FAFC 0%, #EFF6FF 100%);
border: 3px solid #BFDBFE;
border-radius: 22px;
padding: 14px 18px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
position: relative;
flex: 1;
margin: 8px 0;
box-shadow: inset 0 2px 6px rgba(255,255,255,0.9);
}
.detective-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.subj-badge {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 5px 14px;
border-radius: 999px;
font-size: 14px;
font-weight: 900;
border: 2.5px solid;
box-shadow: 0 3px 0 rgba(0,0,0,0.08);
}
.subj-badge.am { background: #FDF2F8; border-color: #F472B6; color: #9D174D; }
.subj-badge.is { background: #EFF6FF; border-color: #60A5FA; color: #1E40AF; }
.subj-badge.are { background: #ECFDF5; border-color: #34D399; color: #065F46; }
.scene-illustration {
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
}
.big-emoji-bubble {
width: 86px;
height: 86px;
border-radius: 24px;
background: #FFFFFF;
border: 4px solid #60A5FA;
box-shadow: 0 7px 0 rgba(37, 99, 235, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 50px;
animation: gentleBob 2.8s ease-in-out infinite;
}
@keyframes gentleBob {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-5px) rotate(2deg); }
}
/* Sentence Display Box */
.sentence-bridge-box {
background: #FFFFFF;
border: 4px solid #60A5FA;
border-radius: 22px;
padding: 16px 20px;
width: 100%;
text-align: center;
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.12);
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 31px;
font-weight: 900;
color: #0F172A;
line-height: 1.3;
}
.subj-highlight {
background: #FEF9C3;
border: 2px dashed #F59E0B;
padding: 2px 10px;
border-radius: 12px;
color: #92400E;
}
.verb-slot {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 112px;
height: 54px;
padding: 0 16px;
border-radius: 16px;
border: 4px dashed #94A3B8;
background: #F1F5F9;
color: #64748B;
font-size: 32px;
font-weight: 900;
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.verb-slot.filled {
border-style: solid;
transform: scale(1.08);
color: #FFFFFF;
box-shadow: 0 6px 0 rgba(0,0,0,0.2);
}
.verb-slot.filled.am { background: linear-gradient(180deg, #F472B6, #DB2777); border-color: #9D174D; }
.verb-slot.filled.is { background: linear-gradient(180deg, #60A5FA, #2563EB); border-color: #1E3A8A; }
.verb-slot.filled.are { background: linear-gradient(180deg, #34D399, #059669); border-color: #064E3B; }
/* Answer Choice Buttons (am, is, are) */
.choices-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
width: 100%;
flex-shrink: 0;
}
.verb-choice-btn {
height: 88px;
border-radius: 22px;
border: 4px solid;
font-family: inherit;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: transform 0.14s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.14s, opacity 0.15s;
position: relative;
}
.verb-choice-btn.am {
background: linear-gradient(180deg, #FDF2F8 0%, #FCE7F3 100%);
border-color: #EC4899;
color: #9D174D;
box-shadow: 0 7px 0 #BE185D;
}
.verb-choice-btn.is {
background: linear-gradient(180deg, #EFF6FF 0%, #DBEAFE 100%);
border-color: #2563EB;
color: #1E40AF;
box-shadow: 0 7px 0 #1D4ED8;
}
.verb-choice-btn.are {
background: linear-gradient(180deg, #ECFDF5 0%, #D1FAE5 100%);
border-color: #059669;
color: #065F46;
box-shadow: 0 7px 0 #047857;
}
.verb-choice-btn:hover:not(:disabled) {
transform: translateY(-3px);
}
.verb-choice-btn:active:not(:disabled) {
transform: translateY(4px);
box-shadow: 0 2px 0 rgba(0,0,0,0.25);
}
.verb-choice-btn.wrong {
opacity: 0.42;
transform: scale(0.95);
filter: grayscale(0.5);
cursor: not-allowed;
}
.verb-choice-btn.correct {
background: linear-gradient(180deg, #4ADE80, #16A34A) !important;
border-color: #14532D !important;
color: #FFFFFF !important;
box-shadow: 0 7px 0 #14532D !important;
transform: scale(1.04);
}
.vc-word {
font-size: 36px;
font-weight: 900;
line-height: 1;
}
.vc-hint {
font-size: 12px;
font-weight: 800;
margin-top: 4px;
opacity: 0.88;
}
/* ============ MODE 2: VERB BASKET SORT ============ */
.sort-stage-wrap {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
gap: 10px;
}
.mystery-bubble-card {
flex: 1;
background: radial-gradient(circle at 50% 35%, #FFFFFF 0%, #EFF6FF 100%);
border: 4px solid #60A5FA;
border-radius: 24px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 14px;
gap: 8px;
position: relative;
box-shadow: 0 8px 22px rgba(37, 99, 235, 0.14);
}
.subject-float-pill {
background: linear-gradient(135deg, #FEF9C3, #FDE047);
border: 4px solid #F59E0B;
border-radius: 26px;
padding: 12px 26px;
display: flex;
align-items: center;
gap: 14px;
box-shadow: 0 8px 0 #D97706;
animation: gentleBob 2.5s ease-in-out infinite;
}
.sf-emoji {
font-size: 48px;
line-height: 1;
}
.sf-text {
font-size: 32px;
font-weight: 900;
color: #78350F;
}
.baskets-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.basket-btn {
height: 126px;
border-radius: 24px;
border: 4px solid;
font-family: inherit;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10px;
transition: transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
}
.basket-btn:hover:not(:disabled) {
transform: translateY(-4px) scale(1.02);
}
.basket-btn.am {
background: linear-gradient(180deg, #FCE7F3 0%, #FBCFE8 100%);
border-color: #DB2777;
color: #831843;
box-shadow: 0 8px 0 #9D174D;
}
.basket-btn.is {
background: linear-gradient(180deg, #DBEAFE 0%, #BFDBFE 100%);
border-color: #2563EB;
color: #1E3A8A;
box-shadow: 0 8px 0 #1E40AF;
}
.basket-btn.are {
background: linear-gradient(180deg, #D1FAE5 0%, #A7F3D0 100%);
border-color: #059669;
color: #064E3B;
box-shadow: 0 8px 0 #065F46;
}
.basket-icon {
font-size: 26px;
line-height: 1;
}
.basket-title {
font-size: 30px;
font-weight: 900;
margin: 2px 0;
}
.basket-sub {
font-size: 12px;
font-weight: 800;
background: rgba(255,255,255,0.75);
padding: 2px 10px;
border-radius: 999px;
}
/* ============ 4. RIGHT SIDE: 3-TEAM CUP CASTLE TOWER RACE ============ */
.race-stage {
background: linear-gradient(180deg, #87CEEB 0%, #E0F2FE 78%, #86EFAC 78%, #22C55E 100%);
border-radius: 24px;
border: 4px solid #60A5FA;
box-shadow: 0 10px 25px rgba(30, 58, 138, 0.15);
padding: 10px 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
overflow: hidden;
}
.rs-head {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255, 255, 255, 0.92);
padding: 6px 10px;
border-radius: 14px;
border: 2px solid #93C5FD;
z-index: 3;
}
.rs-title {
font-size: 15px;
font-weight: 900;
color: #1E3A8A;
display: flex;
align-items: center;
gap: 5px;
}
.towers-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
padding: 8px 0 4px;
position: relative;
z-index: 2;
min-height: 0;
}
.team-column {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
height: 100%;
background: rgba(255, 255, 255, 0.35);
border-radius: 16px;
border: 2px solid rgba(255, 255, 255, 0.65);
padding: 6px 4px;
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
.team-column.active-turn {
background: rgba(255, 255, 255, 0.68);
border: 3px solid #F59E0B;
box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.45);
}
.team-top-pill {
width: 100%;
background: rgba(255, 255, 255, 0.94);
border-radius: 11px;
padding: 4px 5px;
text-align: center;
border: 2px solid;
flex-shrink: 0;
}
.tt-name {
font-size: 12px;
font-weight: 900;
line-height: 1.1;
}
.tt-score-row {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
margin-top: 2px;
font-size: 13px;
font-weight: 900;
color: #1E293B;
}
.minus-cup-btn {
width: 19px;
height: 19px;
border-radius: 50%;
border: 1.5px solid #FCA5A5;
background: #FEE2E2;
color: #DC2626;
font-size: 12px;
font-weight: 900;
line-height: 1;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
}
/* Cup Tower Stack Container */
.cup-stack-area {
flex: 1;
width: 100%;
display: flex;
flex-direction: column-reverse;
align-items: center;
justify-content: flex-start;
padding: 6px 0 2px;
position: relative;
min-height: 0;
overflow: hidden;
}
.pedestal-base {
width: 86%;
height: 10px;
background: linear-gradient(90deg, #64748B, #94A3B8, #64748B);
border: 2px solid #334155;
border-radius: 6px;
flex-shrink: 0;
}
.castle-cup {
width: 68%;
border: 2.5px solid;
border-radius: 5px 5px 12px 12px;
box-shadow: inset 0 -4px 0 rgba(0,0,0,0.18), 0 2px 4px rgba(0,0,0,0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 900;
color: #fff;
position: relative;
margin-top: -2px;
flex-shrink: 0;
animation: dropCup 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes dropCup {
0% { transform: translateY(-28px) scale(1.12); opacity: 0; }
100% { transform: translateY(0) scale(1); opacity: 1; }
}
.leader-crown {
position: absolute;
bottom: 102%;
font-size: 24px;
line-height: 1;
filter: drop-shadow(0 3px 4px rgba(0,0,0,0.3));
animation: gentleBob 1.8s ease-in-out infinite;
pointer-events: none;
}
.team-tap-btns {
display: flex;
gap: 4px;
width: 100%;
flex-shrink: 0;
}
.tap-cup-btn {
flex: 1;
border: 2.5px solid;
border-radius: 11px;
background: #FFFFFF;
font-family: inherit;
font-size: 11.5px;
font-weight: 900;
padding: 5px 2px;
cursor: pointer;
box-shadow: 0 3px 0 rgba(0,0,0,0.16);
transition: transform 0.1s;
}
.tap-cup-btn:active {
transform: translateY(2px);
box-shadow: none;
}
/* Fireworks Canvas / Particles */
.confetti-layer {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 40;
overflow: hidden;
}
.confetti-dot {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
animation: burstOut 0.95s 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; }
}
/* Winner Modal */
.modal-overlay {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.72);
backdrop-filter: blur(5px);
z-index: 80;
display: none;
align-items: center;
justify-content: center;
}
.modal-overlay.open {
display: flex;
}
.winner-card {
background: linear-gradient(180deg, #FFFBEB 0%, #FEF3C7 100%);
border: 6px solid #F59E0B;
border-radius: 30px;
padding: 28px 38px;
text-align: center;
max-width: 520px;
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.5);
}
</style>
</head>
<body>
<div class="viewport-shell">
<div class="stage-1280" id="appStage">
<div class="stage-bg-pattern"></div>
<!-- 1. TOP BAR -->
<header class="top-bar">
<div class="brand-cluster">
<div class="brand-logo" aria-hidden="true">👑</div>
<div>
<div class="brand-title">am, is, are</div>
<div class="brand-sub">Verb to Be Kingdom & Cup Castle Race</div>
</div>
</div>
<nav class="mode-tabs" aria-label="Game Modes">
<button class="mode-tab active" data-mode="bridge" type="button">
<span>🧩</span> <span>1. Sentence Bridge</span>
</button>
<button class="mode-tab" data-mode="sort" type="button">
<span>🧺</span> <span>2. Verb Basket Sort</span>
</button>
<button class="mode-tab" data-mode="flip" type="button">
<span>🔄</span> <span>3. Question Flip</span>
</button>
</nav>
<div class="top-right">
<div class="star-pill" title="Total Correct Sentences">
<span>⭐</span> <span id="starCount">0</span>
</div>
<button class="util-btn" id="repeatBtn" type="button" title="Repeat Question Audio">
<span>🔊</span> Repeat
</button>
<button class="util-btn" id="resetBtn" type="button" title="Reset Scores">
<span>🔄</span> Reset
</button>
<button class="util-btn" id="soundBtn" type="button" title="Toggle Sound">
<span id="soundIcon">🔊</span> Sound
</button>
<button class="util-btn" id="fullBtn" type="button" title="Toggle Fullscreen">
<span>⛶</span> Full
</button>
</div>
</header>
<!-- 2. INTERACTIVE PRONOUN GRAMMAR RULE HELPER & VOICE BAR -->
<section class="grammar-bar" aria-label="Verb to Be Grammar Rules">
<div class="rule-pills">
<button class="rule-pill am" id="ruleBtnAm" type="button" title="Tap to hear the AM rule!">
<span class="rp-verb">am</span>
<span class="rp-subj">👑 Only with <b>I</b><small>"I am..." (Me!)</small></span>
</button>
<button class="rule-pill is" id="ruleBtnIs" type="button" title="Tap to hear the IS rule!">
<span class="rp-verb">is</span>
<span class="rp-subj">🌟 <b>1</b> Person or Thing<small>He / She / It / Singular</small></span>
</button>
<button class="rule-pill are" id="ruleBtnAre" type="button" title="Tap to hear the ARE rule!">
<span class="rp-verb">are</span>
<span class="rp-subj">🎉 <b>2+</b> Plural or You<small>We / They / You / Plural</small></span>
</button>
</div>
<div class="voice-actions">
<button class="act-btn" id="hearClueBtn" type="button">
<span>🇺🇸</span> <span>Hear Clue</span>
</button>
<button class="act-btn uk" id="hearSentenceBtn" type="button">
<span>🇬🇧</span> <span>Hear Sentence</span>
</button>
<button class="act-btn next" id="nextQuestionBtn" type="button">
<span>➡️</span> <span>Next Card</span>
</button>
</div>
</section>
<!-- 3. SPLIT STAGE: LEFT GRAMMAR ARENA + RIGHT CUP CASTLE RACE -->
<main class="main-split">
<section class="grammar-stage" id="grammarStage" aria-label="Interactive Grammar Stage"></section>
<aside class="race-stage" aria-label="3-Team Cup Castle Race">
<div class="rs-head">
<div class="rs-title">
<span>🏰</span> <span>Cup Castle Race</span>
</div>
<button class="act-btn" id="whoWinsBtn" type="button" style="padding:3px 10px;font-size:12px">
🏆 Who's Winning?
</button>
</div>
<div class="towers-grid" id="towersGrid"></div>
<div style="font-size:11.5px;font-weight:800;color:#1E3A8A;text-align:center;background:rgba(255,255,255,0.9);padding:5px;border-radius:11px;border:2px solid #93C5FD;z-index:3" id="raceStatusTxt">
✨ Answering stacks a cup on the active team's castle! Tap any team to switch turns!
</div>
</aside>
</main>
<!-- CONFETTI LAYER -->
<div class="confetti-layer" id="confettiLayer"></div>
<!-- WINNER MODAL -->
<div class="modal-overlay" id="winnerModal" role="dialog" aria-modal="true">
<div class="winner-card">
<div style="font-size:72px;line-height:1;margin-bottom:8px">👑</div>
<h2 style="font-size:34px;font-weight:900;color:#92400E;margin:0 0 6px">Cup Castle Champions!</h2>
<div style="font-size:25px;font-weight:900;color:#1E3A8A;margin:0 0 18px" id="winnerText">Team Ruby Wins!</div>
<div style="display:flex;justify-content:center;gap:12px">
<button class="act-btn" id="closeWinnerBtn" type="button">Keep Playing</button>
<button class="act-btn uk" id="newRaceBtn" type="button">🔄 New Castle Race</button>
</div>
</div>
</div>
</div>
</div>
<script src="assets/audio_am_is_are.js"></script>
<script>
(function() {
"use strict";
/* ============ 1. PROPORTIONAL 1280x720 SCALE 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. ALL 24 SENTENCES & GRAMMAR METADATA ============ */
var QUESTIONS = [
// Community Town
{ id: "01", world: "community", worldName: "🚓 Community Town", emoji: "🚓", subjEmoji: "👮", subject: "The police officer", category: "Singular • Just 1 Person (He/She)", before: "The police officer 👮", after: "helping.", answer: "is", flipSubj: "the police officer 👮", flipAfter: "helping?", shortAns: "Yes, he is!" },
{ id: "02", world: "community", worldName: "🚓 Community Town", emoji: "📚", subjEmoji: "🧑🤝🧑", subject: "We", category: "Plural • 2+ People (Us Together)", before: "We 🧑🤝🧑", after: "at the library.", answer: "are", flipSubj: "we 🧑🤝🧑", flipAfter: "at the library?", shortAns: "Yes, we are!" },
{ id: "03", world: "community", worldName: "🚓 Community Town", emoji: "🛒", subjEmoji: "🙋", subject: "I", category: "First Person • Talking About Myself!", before: "I 🙋", after: "at the supermarket.", answer: "am", flipSubj: "I 🙋", flipAfter: "at the supermarket?", shortAns: "Yes, I am!" },
{ id: "04", world: "community", worldName: "🚓 Community Town", emoji: "🥖", subjEmoji: "🥐", subject: "The bakery", category: "Singular • Just 1 Place (It)", before: "The bakery 🥐", after: "smelling good.", answer: "is", flipSubj: "the bakery 🥐", flipAfter: "smelling good?", shortAns: "Yes, it is!" },
{ id: "05", world: "community", worldName: "🚓 Community Town", emoji: "🏞️", subjEmoji: "🌳", subject: "The park trees", category: "Plural • More Than 1 Tree (They)", before: "The park trees 🌳", after: "very tall.", answer: "are", flipSubj: "the park trees 🌳", flipAfter: "very tall?", shortAns: "Yes, they are!" },
// Sunny Farm
{ id: "06", world: "farm", worldName: "🚜 Sunny Farm", emoji: "🚜", subjEmoji: "🐴", subject: "The horses", category: "Plural • More Than 1 Horse (They)", before: "The horses 🐴", after: "running fast.", answer: "are", flipSubj: "the horses 🐴", flipAfter: "running fast?", shortAns: "Yes, they are!" },
{ id: "07", world: "farm", worldName: "🚜 Sunny Farm", emoji: "🌱", subjEmoji: "🎃", subject: "The pumpkin", category: "Singular • Just 1 Pumpkin (It)", before: "The pumpkin 🎃", after: "growing in the soil.", answer: "is", flipSubj: "the pumpkin 🎃", flipAfter: "growing in the soil?", shortAns: "Yes, it is!" },
{ id: "08", world: "farm", worldName: "🚜 Sunny Farm", emoji: "🌻", subjEmoji: "🧑🌾", subject: "I", category: "First Person • Talking About Myself!", before: "I 🧑🌾", after: "planting seeds.", answer: "am", flipSubj: "I 🧑🌾", flipAfter: "planting seeds?", shortAns: "Yes, I am!" },
{ id: "09", world: "farm", worldName: "🚜 Sunny Farm", emoji: "🐑", subjEmoji: "🐑", subject: "The sheep", category: "Singular • 1 Fluffy Sheep (It)", before: "The sheep 🐑", after: "fluffy.", answer: "is", flipSubj: "the sheep 🐑", flipAfter: "fluffy?", shortAns: "Yes, it is!" },
// Cozy Closet
{ id: "10", world: "clothing", worldName: "👕 Cozy Closet", emoji: "🧥", subjEmoji: "🥾", subject: "My boots", category: "Plural • 2 Muddy Boots (They)", before: "My boots 🥾", after: "muddy.", answer: "are", flipSubj: "my boots 🥾", flipAfter: "muddy?", shortAns: "Yes, they are!" },
{ id: "11", world: "clothing", worldName: "👕 Cozy Closet", emoji: "👗", subjEmoji: "👒", subject: "Her hat", category: "Singular • Just 1 Hat (It)", before: "Her hat 👒", after: "yellow.", answer: "is", flipSubj: "her hat 👒", flipAfter: "yellow?", shortAns: "Yes, it is!" },
{ id: "12", world: "clothing", worldName: "👕 Cozy Closet", emoji: "👕", subjEmoji: "👕", subject: "I", category: "First Person • Talking About Myself!", before: "I 👕", after: "wearing a blue shirt.", answer: "am", flipSubj: "I 👕", flipAfter: "wearing a blue shirt?", shortAns: "Yes, I am!" },
{ id: "13", world: "clothing", worldName: "👕 Cozy Closet", emoji: "🧦", subjEmoji: "🧦", subject: "These socks", category: "Plural • 2 Warm Socks (They)", before: "These socks 🧦", after: "warm.", answer: "are", flipSubj: "these socks 🧦", flipAfter: "warm?", shortAns: "Yes, they are!" },
{ id: "14", world: "clothing", worldName: "👕 Cozy Closet", emoji: "🧣", subjEmoji: "🧥", subject: "The jacket", category: "Singular • Just 1 Jacket (It)", before: "The jacket 🧥", after: "big.", answer: "is", flipSubj: "the jacket 🧥", flipAfter: "big?", shortAns: "Yes, it is!" },
// Yummy Kitchen & Friends
{ id: "15", world: "food", worldName: "🍓 Food & Friends", emoji: "🥩", subjEmoji: "🐟", subject: "Fish", category: "Singular Food Group (It)", before: "Fish 🐟", after: "healthy protein.", answer: "is", flipSubj: "fish 🐟", flipAfter: "healthy protein?", shortAns: "Yes, it is!" },
{ id: "16", world: "food", worldName: "🍓 Food & Friends", emoji: "🥪", subjEmoji: "🍞", subject: "I", category: "First Person • Talking About Myself!", before: "I 🍞", after: "eating whole grain bread.", answer: "am", flipSubj: "I 🍞", flipAfter: "eating whole grain bread?", shortAns: "Yes, I am!" },
{ id: "17", world: "food", worldName: "🍓 Food & Friends", emoji: "🍎", subjEmoji: "🍓", subject: "The strawberries", category: "Plural • Lots of Berries (They)", before: "The strawberries 🍓", after: "sweet fruits.", answer: "are", flipSubj: "the strawberries 🍓", flipAfter: "sweet fruits?", shortAns: "Yes, they are!" },
{ id: "18", world: "food", worldName: "🍓 Food & Friends", emoji: "🧀", subjEmoji: "🥛", subject: "Milk", category: "Singular Drink (It)", before: "Milk 🥛", after: "good for my bones.", answer: "is", flipSubj: "milk 🥛", flipAfter: "good for my bones?", shortAns: "Yes, it is!" },
{ id: "19", world: "food", worldName: "🍓 Food & Friends", emoji: "🧥", subjEmoji: "👧", subject: "She", category: "Singular • Just 1 Girl (She)", before: "She 👧", after: "wearing a pink raincoat.", answer: "is", flipSubj: "she 👧", flipAfter: "wearing a pink raincoat?", shortAns: "Yes, she is!" },
{ id: "20", world: "food", worldName: "🍓 Food & Friends", emoji: "🐑", subjEmoji: "🐑", subject: "Three little lambs", category: "Plural • 3 Little Lambs (2+)", before: "There", after: "three little lambs 🐑🐑🐑.", answer: "are", flipSubj: "there", flipAfter: "three little lambs 🐑🐑🐑?", shortAns: "Yes, there are!" },
{ id: "21", world: "food", worldName: "🍓 Food & Friends", emoji: "🥕", subjEmoji: "🙋", subject: "I", category: "First Person • Talking About Myself!", before: "I 🙋", after: "picking carrots for my rabbit 🐇.", answer: "am", flipSubj: "I 🙋", flipAfter: "picking carrots for my rabbit 🐇?", shortAns: "Yes, I am!" },
{ id: "22", world: "food", worldName: "🍓 Food & Friends", emoji: "🥚", subjEmoji: "🐊", subject: "A baby crocodile", category: "Singular • 1 Baby Crocodile (It)", before: "There", after: "a baby crocodile 🐊 inside the egg.", answer: "is", flipSubj: "there", flipAfter: "a baby crocodile 🐊 inside the egg?", shortAns: "Yes, there is!" },
{ id: "23", world: "food", worldName: "🍓 Food & Friends", emoji: "🏥", subjEmoji: "👩", subject: "My mother", category: "Singular • 1 Person (She)", before: "My mother 👩", after: "going to the hospital to see a doctor.", answer: "is", flipSubj: "my mother 👩", flipAfter: "going to the hospital?", shortAns: "Yes, she is!" },
{ id: "24", world: "food", worldName: "🍓 Food & Friends", emoji: "📚", subjEmoji: "🧑🤝🧑", subject: "They", category: "Plural • More Than 1 Person (They)", before: "They 🧑🤝🧑", after: "at the library to read books 📚.", answer: "are", flipSubj: "they 🧑🤝🧑", flipAfter: "at the library to read books 📚?", shortAns: "Yes, they are!" }
];
var TEAMS = [
{ name: "Team Ruby", icon: "🔴", c: "#EF4444", bg: "linear-gradient(180deg, #F87171, #DC2626)", bd: "#991B1B" },
{ name: "Team Sapphire", icon: "🔵", c: "#2563EB", bg: "linear-gradient(180deg, #60A5FA, #2563EB)", bd: "#1E3A8A" },
{ name: "Team Emerald", icon: "🟢", c: "#10B981", bg: "linear-gradient(180deg, #34D399, #059669)", bd: "#064E3B" }
];
/* ============ 3. STATE ============ */
var state = {
mode: "bridge", // "bridge" | "sort" | "flip"
worldFilter: "all", // "all" | "community" | "farm" | "clothing" | "food"
qIdx: 0,
solved: false,
wrongGuesses: [],
streak: 0,
stars: 0,
activeTeam: 0,
teamScores: [0, 0, 0],
soundOn: true,
audioCtx: null,
decoded: {},
activeSources: [],
activeHtmlAudio: null
};
function getFilteredList() {
if (state.worldFilter === "all") return QUESTIONS;
return QUESTIONS.filter(function(item) { return item.world === state.worldFilter; });
}
function currentItem() {
var list = getFilteredList();
if (!list.length) return QUESTIONS[0];
var idx = ((state.qIdx % list.length) + list.length) % list.length;
return list[idx];
}
/* ============ 4. STUDIO VOICE ENGINE (JENNY US FEMALE + RYAN UK MALE) ============ */
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 playTone(freq, dur, type) {
if (!state.soundOn) return;
var ctx = ensureAudio();
if (!ctx) return;
try {
var o = ctx.createOscillator();
var g = ctx.createGain();
var t = ctx.currentTime;
o.type = type || "sine";
o.frequency.setValueAtTime(freq, t);
o.frequency.exponentialRampToValueAtTime(freq * 1.32, t + (dur || 0.15));
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(0.09, t + 0.015);
g.gain.exponentialRampToValueAtTime(0.0001, t + (dur || 0.15));
o.connect(g);
g.connect(ctx.destination);
o.start(t);
o.stop(t + (dur || 0.15) + 0.02);
} 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.AM_IS_ARE_AUDIO && window.AM_IS_ARE_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 playClips(keys) {
if (!state.soundOn) return;
stopAllSpeech();
var valid = (keys || []).filter(function(k) {
return window.AM_IS_ARE_AUDIO && window.AM_IS_ARE_AUDIO[k];
});
if (!valid.length) return;
var ctx = ensureAudio();
if (ctx) {
try {
var bufs = await Promise.all(valid.map(decodeClip));
var cursor = ctx.currentTime + 0.02;
bufs.forEach(function(buf) {
if (!buf) return;
var span = buf._trimSpan || [0, buf.duration];
var start = span[0];
var dur = Math.max(0.08, span[1] - start);
var src = ctx.createBufferSource();
src.buffer = buf;
src.connect(ctx.destination);
src.start(cursor, start, dur);
state.activeSources.push(src);
cursor += dur + 0.05;
});
return;
} catch (e) {}
}
for (var i = 0; i < valid.length; i++) {
var item = window.AM_IS_ARE_AUDIO[valid[i]];
if (!item) continue;
await new Promise(function(resolve) {
var a = new Audio(item.src);
state.activeHtmlAudio = a;
a.onended = resolve;
a.onerror = resolve;
a.play().catch(resolve);
});
}
}
function playClip(key) {
return playClips([key]);
}
/* ============ 5. CONFETTI / FIREWORKS BURST ============ */
function triggerFireworks() {
var layer = document.getElementById("confettiLayer");
if (!layer) return;
var colors = ["#F59E0B", "#EC4899", "#3B82F6", "#10B981", "#A855F7", "#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() * 220;
dot.style.left = "42%";
dot.style.top = "48%";
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);
}, 1000, dot);
}
}
/* ============ 6. RENDER GRAMMAR RULE PILLS HIGHLIGHT ============ */
function updateRuleHighlight() {
var item = currentItem();
["am", "is", "are"].forEach(function(v) {
var btn = document.getElementById("ruleBtn" + v.charAt(0).toUpperCase() + v.slice(1));
if (!btn) return;
if (state.solved && item.answer === v) {
btn.classList.add("highlight");
} else {
btn.classList.remove("highlight");
}
});
}
/* ============ 7. HANDLE OPTION PICK across all 3 modes ============ */
function handlePickVerb(selected) {
if (state.solved) return;
var item = currentItem();
if (selected === item.answer) {
state.solved = true;
state.stars += 1;
state.streak += 1;
document.getElementById("starCount").textContent = String(state.stars);
// Award a Cup to the active team and advance active team turn!
var rewardedTeam = state.activeTeam;
state.teamScores[rewardedTeam] += 1;
state.activeTeam = (state.activeTeam + 1) % 3;
playTone(587.33, 0.14, "sine");
setTimeout(function() { playTone(880, 0.18, "triangle"); }, 90);
triggerFireworks();
var statusEl = document.getElementById("raceStatusTxt");
if (statusEl) {
statusEl.innerHTML = "🎉 <b>" + TEAMS[rewardedTeam].name + "</b> stacked Cup #" + state.teamScores[rewardedTeam] + "! Next up: <b>" + TEAMS[state.activeTeam].name + "</b>!";
}
// Speak the completed or flipped sentence with British Male Ryan!
if (state.mode === "flip") {
playClip("flip_" + item.id);
} else {
playClip("ans_" + item.id);
}
renderAll();
} else {
if (state.wrongGuesses.indexOf(selected) === -1) {
state.wrongGuesses.push(selected);
}
state.streak = 0;
playTone(210, 0.22, "sawtooth");
playClip("try_again");
renderStage();
}
}
function nextQuestion(speakClueAfter) {
var list = getFilteredList();
state.qIdx = (state.qIdx + 1) % list.length;
state.solved = false;
state.wrongGuesses = [];
renderAll();
if (speakClueAfter) {
speakCurrentPrompt();
}
}
function speakCurrentPrompt() {
var item = currentItem();
if (state.mode === "sort") {
playClip("sort_" + item.id);
} else if (state.mode === "flip" && state.solved) {
playClip("flip_" + item.id);
} else if (state.solved) {
playClip("ans_" + item.id);
} else {
playClip("q_" + item.id);
}
}
/* ============ 8. RENDER LEFT GRAMMAR STAGE ============ */
function renderThemeHeader(list) {
var idx = ((state.qIdx % list.length) + list.length) % list.length;
var worlds = [
{ id: "all", lbl: "🌎 All 24 Cards" },
{ id: "community", lbl: "🚓 Community" },
{ id: "farm", lbl: "🚜 Sunny Farm" },
{ id: "clothing", lbl: "👕 Cozy Closet" },
{ id: "food", lbl: "🍓 Food & Friends" }
];
var chipsHtml = worlds.map(function(w) {
var active = state.worldFilter === w.id ? " active" : "";
return '<button type="button" class="theme-chip' + active + '" data-world="' + w.id + '">' + w.lbl + '</button>';
}).join("");
return (
'<div class="theme-row">' +
'<div class="theme-chips">' + chipsHtml + '</div>' +
'<div class="q-counter-badge">Card ' + (idx + 1) + ' of ' + list.length + ' • Streak 🔥 ' + state.streak + '</div>' +
'</div>'
);
}
function renderBridgeMode(stage, item, list) {
var slotClass = "verb-slot" + (state.solved ? (" filled " + item.answer) : "");
var slotText = state.solved ? item.answer : "___";
var options = [
{ v: "am", hint: "👑 Only with I" },
{ v: "is", hint: "🌟 1 Person / Thing" },
{ v: "are", hint: "🎉 2+ Plural / You" }
];
var btnsHtml = options.map(function(o) {
var isWrong = state.wrongGuesses.indexOf(o.v) !== -1;
var isCorrect = state.solved && item.answer === o.v;
var cls = "verb-choice-btn " + o.v + (isWrong ? " wrong" : "") + (isCorrect ? " correct" : "");
var dis = (isWrong || state.solved) ? " disabled" : "";
return (
'<button type="button" class="' + cls + '" data-pick="' + o.v + '"' + dis + '>' +
'<span class="vc-word">' + o.v + '</span>' +
'<span class="vc-hint">' + o.hint + '</span>' +
'</button>'
);
}).join("");
stage.innerHTML =
renderThemeHeader(list) +
'<div class="scene-card">' +
'<div class="detective-row">' +
'<span class="subj-badge ' + item.answer + '">🔎 Subject Detective: <b>"' + item.subject + '"</b> • ' + item.category + '</span>' +
'</div>' +
'<div class="scene-illustration">' +
'<div class="big-emoji-bubble">' + item.subjEmoji + '</div>' +
'<div style="font-size:32px;font-weight:900;color:#94A3B8">+</div>' +
'<div class="big-emoji-bubble" style="border-color:#F59E0B">' + item.emoji + '</div>' +
'</div>' +
'<div class="sentence-bridge-box">' +
'<span class="subj-highlight">' + item.before + '</span>' +
'<span class="' + slotClass + '">' + slotText + '</span>' +
'<span>' + item.after + '</span>' +
'</div>' +
'</div>' +
'<div class="choices-row">' + btnsHtml + '</div>';
}
function renderSortMode(stage, item, list) {
var baskets = [
{ v: "am", icon: "👑", title: "AM Basket", sub: "Only for I (Myself!)" },
{ v: "is", icon: "🌟", title: "IS Basket", sub: "He / She / It (Just 1)" },
{ v: "are", icon: "🎉", title: "ARE Basket", sub: "We / They / You (2+)" }
];
var basketsHtml = baskets.map(function(b) {
var isWrong = state.wrongGuesses.indexOf(b.v) !== -1;
var isCorrect = state.solved && item.answer === b.v;
var extraStyle = isWrong ? "opacity:0.4;transform:scale(0.95);" : (isCorrect ? "border-color:#16A34A;background:linear-gradient(180deg,#86EFAC,#22C55E);color:#fff;" : "");
var dis = (isWrong || state.solved) ? " disabled" : "";
return (
'<button type="button" class="basket-btn ' + b.v + '" style="' + extraStyle + '" data-pick="' + b.v + '"' + dis + '>' +
'<span class="basket-icon">' + b.icon + ' 🧺</span>' +
'<span class="basket-title">' + b.title + '</span>' +
'<span class="basket-sub">' + b.sub + '</span>' +
'</button>'
);
}).join("");
var feedbackHtml = state.solved
? '<div style="font-size:22px;font-weight:900;color:#047857;background:#D1FAE5;border:3px solid #34D399;padding:8px 20px;border-radius:16px">✅ ' + item.before + ' <u style="color:#1E40AF">' + item.answer.toUpperCase() + '</u> ' + item.after + '</div>'
: '<div style="font-size:16px;font-weight:800;color:#475569">👇 Tap the matching Verb Basket below to toss this subject bubble in!</div>';
stage.innerHTML =
renderThemeHeader(list) +
'<div class="sort-stage-wrap">' +
'<div class="mystery-bubble-card">' +
'<div style="font-size:13.5px;font-weight:900;color:#2563EB;text-transform:uppercase;letter-spacing:0.6px">🫧 Mystery Subject Bubble</div>' +
'<div class="subject-float-pill">' +
'<span class="sf-emoji">' + item.subjEmoji + '</span>' +
'<span class="sf-text">' + item.subject + '</span>' +
'</div>' +
'<div class="subj-badge ' + item.answer + '">' + item.category + '</div>' +
feedbackHtml +
'</div>' +
'<div class="baskets-row">' + basketsHtml + '</div>' +
'</div>';
}
function renderFlipMode(stage, item, list) {
var capAnswer = item.answer.charAt(0).toUpperCase() + item.answer.slice(1);
var slotClass = "verb-slot" + (state.solved ? (" filled " + item.answer) : "");
var slotText = state.solved ? capAnswer : "___";
var options = [
{ v: "am", word: "Am", hint: "👑 Am I...?" },
{ v: "is", word: "Is", hint: "🌟 Is he / she / it...?" },
{ v: "are", word: "Are", hint: "🎉 Are we / they / you...?" }
];
var btnsHtml = options.map(function(o) {
var isWrong = state.wrongGuesses.indexOf(o.v) !== -1;
var isCorrect = state.solved && item.answer === o.v;
var cls = "verb-choice-btn " + o.v + (isWrong ? " wrong" : "") + (isCorrect ? " correct" : "");
var dis = (isWrong || state.solved) ? " disabled" : "";
return (
'<button type="button" class="' + cls + '" data-pick="' + o.v + '"' + dis + '>' +
'<span class="vc-word">' + o.word + '...?</span>' +
'<span class="vc-hint">' + o.hint + '</span>' +
'</button>'
);
}).join("");
stage.innerHTML =
renderThemeHeader(list) +
'<div class="scene-card">' +
'<div style="font-size:14px;font-weight:900;color:#6D28D9;background:#F3E8FF;border:2px solid #C084FC;padding:5px 14px;border-radius:999px">' +
'🔄 Statement: <b>"' + item.before + ' <span style="color:#2563EB;text-decoration:underline">' + item.answer + '</span> ' + item.after + '"</b> → Flip the verb to the front!' +
'</div>' +
'<div class="sentence-bridge-box">' +
'<span class="' + slotClass + '">' + slotText + '</span>' +
'<span class="subj-highlight">' + item.flipSubj + '</span>' +
'<span>' + item.flipAfter + '</span>' +
'</div>' +
(state.solved
? '<div style="font-size:22px;font-weight:900;color:#065F46;background:#D1FAE5;border:3px solid #34D399;padding:6px 18px;border-radius:14px">🇬🇧 Short Answer: <b>"' + item.shortAns + '"</b> ' + item.emoji + '</div>'
: '<div style="font-size:14.5px;font-weight:800;color:#475569">💡 Which verb jumps to the very beginning to turn this into a question?</div>'
) +
'</div>' +
'<div class="choices-row">' + btnsHtml + '</div>';
}
function renderStage() {
var stage = document.getElementById("grammarStage");
if (!stage) return;
var list = getFilteredList();
var item = currentItem();
if (state.mode === "sort") {
renderSortMode(stage, item, list);
} else if (state.mode === "flip") {
renderFlipMode(stage, item, list);
} else {
renderBridgeMode(stage, item, list);
}
// Bind world filter chips
stage.querySelectorAll("[data-world]").forEach(function(btn) {
btn.addEventListener("click", function() {
state.worldFilter = btn.getAttribute("data-world");
state.qIdx = 0;
state.solved = false;
state.wrongGuesses = [];
playTone(520, 0.09, "sine");
renderAll();
});
});
// Bind verb pick buttons
stage.querySelectorAll("[data-pick]").forEach(function(btn) {
btn.addEventListener("click", function() {
handlePickVerb(btn.getAttribute("data-pick"));
});
});
updateRuleHighlight();
}
/* ============ 9. RENDER RIGHT 3-TEAM CUP CASTLE RACE ============ */
function renderRaceTowers() {
var grid = document.getElementById("towersGrid");
if (!grid) return;
var maxScore = Math.max.apply(null, state.teamScores);
var scaleMax = Math.max(8, maxScore + 1);
grid.innerHTML = TEAMS.map(function(t, idx) {
var score = state.teamScores[idx];
var isLeader = maxScore > 0 && score === maxScore;
var isTurn = state.activeTeam === idx;
var cupsHtml = "";
for (var c = 0; c < score; c++) {
var isTop = c === score - 1;
var hPct = Math.max(16, Math.floor(270 / scaleMax));
cupsHtml +=
'<div class="castle-cup" style="height:' + hPct + 'px;background:' + t.bg + ';border-color:' + t.bd + ';z-index:' + (c + 2) + '">' +
(scaleMax <= 18 ? (c + 1) : "") +
(isTop && isLeader ? '<span class="leader-crown">👑</span>' : "") +
'</div>';
}
return (
'<div class="team-column' + (isTurn ? ' active-turn' : '') + '" data-team-col="' + idx + '">' +
'<div class="team-top-pill" style="border-color:' + t.c + '">' +
'<div class="tt-name" style="color:' + t.c + '">' + t.icon + ' ' + t.name + '</div>' +
'<div class="tt-score-row">' +
'<span>🥤 ' + score + '</span>' +
'<button type="button" class="minus-cup-btn" data-minus="' + idx + '" title="Remove 1 cup">-</button>' +
'</div>' +
'</div>' +
'<div class="cup-stack-area">' +
'<div class="pedestal-base"></div>' +
cupsHtml +
'</div>' +
'<div class="team-tap-btns">' +
'<button type="button" class="tap-cup-btn" style="border-color:' + t.c + ';color:' + t.c + '" data-add="' + idx + '">✋ +1 Cup</button>' +
'</div>' +
'</div>'
);
}).join("");
// Bind column click to switch active team
grid.querySelectorAll("[data-team-col]").forEach(function(col) {
col.addEventListener("click", function(e) {
if (e.target.closest("button")) return;
state.activeTeam = parseInt(col.getAttribute("data-team-col"), 10);
playTone(440, 0.08, "sine");
renderRaceTowers();
});
});
// Bind +1 cup button
grid.querySelectorAll("[data-add]").forEach(function(btn) {
btn.addEventListener("click", function(e) {
e.stopPropagation();
var ti = parseInt(btn.getAttribute("data-add"), 10);
state.teamScores[ti] += 1;
playTone(620, 0.12, "triangle");
playClip("praise_" + (ti + 1));
renderRaceTowers();
});
});
// Bind -1 cup button
grid.querySelectorAll("[data-minus]").forEach(function(btn) {
btn.addEventListener("click", function(e) {
e.stopPropagation();
var ti = parseInt(btn.getAttribute("data-minus"), 10);
state.teamScores[ti] = Math.max(0, state.teamScores[ti] - 1);
playTone(280, 0.09, "sine");
renderRaceTowers();
});
});
}
function renderAll() {
renderStage();
renderRaceTowers();
}
/* ============ 10. WIRE GLOBAL CONTROLS ============ */
document.querySelectorAll(".mode-tab").forEach(function(tab) {
tab.addEventListener("click", function() {
var m = tab.getAttribute("data-mode");
state.mode = m;
state.solved = false;
state.wrongGuesses = [];
document.querySelectorAll(".mode-tab").forEach(function(t) {
t.classList.toggle("active", t === tab);
});
if (m === "bridge") playClip("mode_bridge");
else if (m === "sort") playClip("mode_sort");
else if (m === "flip") playClip("mode_flip");
renderAll();
});
});
document.getElementById("ruleBtnAm").addEventListener("click", function() {
playClip("rule_am");
});
document.getElementById("ruleBtnIs").addEventListener("click", function() {
playClip("rule_is");
});
document.getElementById("ruleBtnAre").addEventListener("click", function() {
playClip("rule_are");
});
document.getElementById("hearClueBtn").addEventListener("click", function() {
var item = currentItem();
playClip(state.mode === "sort" ? ("sort_" + item.id) : ("q_" + item.id));
});
document.getElementById("hearSentenceBtn").addEventListener("click", function() {
var item = currentItem();
playClip(state.mode === "flip" ? ("flip_" + item.id) : ("ans_" + item.id));
});
document.getElementById("nextQuestionBtn").addEventListener("click", function() {
nextQuestion(true);
});
document.getElementById("repeatBtn").addEventListener("click", function() {
speakCurrentPrompt();
});
document.getElementById("resetBtn").addEventListener("click", function() {
state.teamScores = [0, 0, 0];
state.stars = 0;
state.streak = 0;
state.solved = false;
state.wrongGuesses = [];
document.getElementById("starCount").textContent = "0";
playTone(360, 0.12, "sine");
renderAll();
});
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() {});
}
});
document.getElementById("whoWinsBtn").addEventListener("click", function() {
var max = Math.max.apply(null, state.teamScores);
var winners = TEAMS.filter(function(_, i) { return state.teamScores[i] === max; });
var names = winners.map(function(w) { return w.icon + " " + w.name; }).join(" & ");
document.getElementById("winnerText").textContent = max === 0
? "Stack some cups first to crown a champion!"
: names + " (" + max + " Cups!)";
document.getElementById("winnerModal").classList.add("open");
triggerFireworks();
playClip("winner");
});
document.getElementById("closeWinnerBtn").addEventListener("click", function() {
document.getElementById("winnerModal").classList.remove("open");
});
document.getElementById("newRaceBtn").addEventListener("click", function() {
state.teamScores = [0, 0, 0];
document.getElementById("winnerModal").classList.remove("open");
renderRaceTowers();
});
// Initial render
renderAll();
})();
</script>
</body>
</html>