Create index.html
Browse files- index.html +876 -0
index.html
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
| 6 |
+
<title>Copilot Quest — Adventure Quiz</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--bg: #0b1020;
|
| 10 |
+
--panel: rgba(255,255,255,.06);
|
| 11 |
+
--text: rgba(255,255,255,.92);
|
| 12 |
+
--muted: rgba(255,255,255,.70);
|
| 13 |
+
--border: rgba(255,255,255,.12);
|
| 14 |
+
--shadow: 0 12px 30px rgba(0,0,0,.35);
|
| 15 |
+
--radius: 18px;
|
| 16 |
+
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 17 |
+
--sans: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
| 18 |
+
}
|
| 19 |
+
html, body {
|
| 20 |
+
height: 100%;
|
| 21 |
+
margin: 0;
|
| 22 |
+
background: radial-gradient(1200px 900px at 20% 10%, rgba(120,200,255,.15), transparent 60%),
|
| 23 |
+
radial-gradient(900px 700px at 80% 40%, rgba(200,120,255,.12), transparent 60%),
|
| 24 |
+
radial-gradient(900px 700px at 40% 90%, rgba(120,255,200,.10), transparent 60%),
|
| 25 |
+
var(--bg);
|
| 26 |
+
color: var(--text);
|
| 27 |
+
font-family: var(--sans);
|
| 28 |
+
}
|
| 29 |
+
.wrap { max-width: 1100px; margin: 0 auto; padding: 18px; box-sizing: border-box; }
|
| 30 |
+
header { display:flex; gap:12px; align-items:center; justify-content:space-between; margin-bottom:14px; }
|
| 31 |
+
.brand { display:flex; gap:12px; align-items:center; }
|
| 32 |
+
.logo {
|
| 33 |
+
width: 44px; height: 44px; border-radius: 14px;
|
| 34 |
+
background: linear-gradient(135deg, rgba(120,200,255,.35), rgba(200,120,255,.25));
|
| 35 |
+
border: 1px solid var(--border); box-shadow: var(--shadow);
|
| 36 |
+
display:grid; place-items:center; font-weight:800; letter-spacing:.5px;
|
| 37 |
+
}
|
| 38 |
+
h1 { font-size: 18px; margin: 0; line-height: 1.1; }
|
| 39 |
+
.subtitle { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
| 40 |
+
.grid { display:grid; grid-template-columns: 1.6fr .9fr; gap:14px; }
|
| 41 |
+
@media (max-width: 980px) { .grid { grid-template-columns: 1fr; } }
|
| 42 |
+
.card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); overflow:hidden; }
|
| 43 |
+
.card .hd { padding: 14px 16px; background: rgba(255,255,255,.04); border-bottom: 1px solid var(--border); display:flex; gap:10px; align-items:center; justify-content:space-between; }
|
| 44 |
+
.card .bd { padding: 16px; }
|
| 45 |
+
.btnrow { display:flex; gap:8px; flex-wrap:wrap; justify-content:flex-end; }
|
| 46 |
+
button {
|
| 47 |
+
border: 1px solid var(--border); background: rgba(255,255,255,.06); color: var(--text);
|
| 48 |
+
border-radius: 14px; padding: 10px 12px; cursor: pointer; transition: transform .06s ease, background .12s ease;
|
| 49 |
+
font-weight: 600;
|
| 50 |
+
}
|
| 51 |
+
button:hover { background: rgba(255,255,255,.10); }
|
| 52 |
+
button:active { transform: translateY(1px); }
|
| 53 |
+
button.primary { background: rgba(120,200,255,.18); border-color: rgba(120,200,255,.30); }
|
| 54 |
+
button.danger { background: rgba(255,120,140,.12); border-color: rgba(255,120,140,.26); }
|
| 55 |
+
button.small { padding: 7px 10px; border-radius: 12px; font-size: 12px; }
|
| 56 |
+
.stats { display:grid; grid-template-columns: 1fr 1fr; gap:10px; }
|
| 57 |
+
.stat { padding: 12px; background: rgba(255,255,255,.05); border: 1px solid var(--border); border-radius: 16px; }
|
| 58 |
+
.stat .k { font-size: 12px; color: var(--muted); }
|
| 59 |
+
.stat .v { font-size: 18px; font-weight: 800; margin-top: 2px; font-family: var(--mono); }
|
| 60 |
+
.hearts { letter-spacing: 2px; font-size: 16px; margin-top: 4px; font-family: var(--mono); }
|
| 61 |
+
.pill { display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:999px; border:1px solid var(--border); background: rgba(255,255,255,.05); font-size:12px; color: var(--muted); }
|
| 62 |
+
.question { font-size: 15px; line-height: 1.45; white-space: pre-wrap; margin: 0 0 14px 0; }
|
| 63 |
+
.hint { color: var(--muted); font-size: 12px; margin-top: -4px; margin-bottom: 10px; }
|
| 64 |
+
.choices { display:grid; gap: 10px; }
|
| 65 |
+
label.choice {
|
| 66 |
+
display:flex; gap:10px; align-items:flex-start; padding: 12px;
|
| 67 |
+
border-radius: 16px; border: 1px solid var(--border); background: rgba(255,255,255,.04);
|
| 68 |
+
cursor: pointer;
|
| 69 |
+
}
|
| 70 |
+
label.choice:hover { background: rgba(255,255,255,.07); }
|
| 71 |
+
.choice .letter {
|
| 72 |
+
width: 30px; height: 30px; border-radius: 12px;
|
| 73 |
+
background: rgba(255,255,255,.07); border: 1px solid var(--border);
|
| 74 |
+
display:grid; place-items:center; font-weight:800; font-family: var(--mono); flex: 0 0 auto;
|
| 75 |
+
}
|
| 76 |
+
.choice .txt { color: var(--text); line-height: 1.35; font-size: 14px; }
|
| 77 |
+
.msg { margin-top: 12px; padding: 10px 12px; border-radius: 14px; border: 1px solid var(--border); background: rgba(255,255,255,.05); white-space: pre-wrap; }
|
| 78 |
+
.msg.good { border-color: rgba(120,255,180,.35); }
|
| 79 |
+
.msg.bad { border-color: rgba(255,120,140,.35); }
|
| 80 |
+
.msg.warn { border-color: rgba(255,210,120,.35); }
|
| 81 |
+
.map { display:grid; gap: 10px; }
|
| 82 |
+
.node { padding: 12px; border-radius: 16px; border: 1px solid var(--border); background: rgba(255,255,255,.04); }
|
| 83 |
+
.node .t { font-weight: 800; margin-bottom: 4px; }
|
| 84 |
+
.node .d { color: var(--muted); font-size: 12px; line-height: 1.35; }
|
| 85 |
+
.node.active { border-color: rgba(120,200,255,.35); background: rgba(120,200,255,.10); }
|
| 86 |
+
.node.done { border-color: rgba(120,255,180,.35); background: rgba(120,255,180,.08); }
|
| 87 |
+
.inventory { display:grid; gap: 8px; margin-top: 10px; }
|
| 88 |
+
.item { padding: 10px 12px; border-radius: 14px; border: 1px solid var(--border); background: rgba(255,255,255,.04); font-size: 13px; }
|
| 89 |
+
.item .name { font-weight: 800; margin-bottom: 2px; }
|
| 90 |
+
.item .desc { color: var(--muted); font-size: 12px; line-height: 1.35; }
|
| 91 |
+
.foot { color: var(--muted); font-size: 12px; margin-top: 10px; line-height: 1.35; }
|
| 92 |
+
.kbd { font-family: var(--mono); font-size: 12px; padding: 2px 6px; border-radius: 8px; border: 1px solid var(--border); background: rgba(255,255,255,.05); }
|
| 93 |
+
|
| 94 |
+
/* --- music controls --- */
|
| 95 |
+
#musicVol{
|
| 96 |
+
width: 140px;
|
| 97 |
+
height: 28px;
|
| 98 |
+
vertical-align: middle;
|
| 99 |
+
accent-color: rgba(120,200,255,.85);
|
| 100 |
+
opacity: .92;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
</style>
|
| 104 |
+
</head>
|
| 105 |
+
<body>
|
| 106 |
+
<div class="wrap">
|
| 107 |
+
<header>
|
| 108 |
+
<div class="brand">
|
| 109 |
+
<div class="logo">CQ</div>
|
| 110 |
+
<div>
|
| 111 |
+
<h1>Copilot Quest</h1>
|
| 112 |
+
<div class="subtitle">Adventure-styled multiple-choice practice game (offline, single file)</div>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
<div class="btnrow">
|
| 116 |
+
<button class="small" id="btnSound">Sound: On</button>
|
| 117 |
+
<button class="small" id="btnMusic">Music: On</button>
|
| 118 |
+
<input id="musicVol" type="range" min="0" max="100" value="25" title="Music volume" />
|
| 119 |
+
<button class="small" id="btnSave">Save</button>
|
| 120 |
+
<button class="small" id="btnLoad">Load</button>
|
| 121 |
+
<button class="small danger" id="btnReset">Reset</button>
|
| 122 |
+
</div>
|
| 123 |
+
</header>
|
| 124 |
+
|
| 125 |
+
<div class="grid">
|
| 126 |
+
<section class="card">
|
| 127 |
+
<div class="hd">
|
| 128 |
+
<div>
|
| 129 |
+
<span class="pill" id="badgeRegion">Region: —</span>
|
| 130 |
+
<span class="pill" id="badgeProgress">Progress: —</span>
|
| 131 |
+
</div>
|
| 132 |
+
<div class="btnrow">
|
| 133 |
+
<button class="primary" id="btnPrimary">Start</button>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
<div class="bd">
|
| 137 |
+
<p class="question" id="story"></p>
|
| 138 |
+
<div id="qArea" style="display:none;">
|
| 139 |
+
<p class="question" id="qText"></p>
|
| 140 |
+
<div class="hint" id="qHint"></div>
|
| 141 |
+
<div class="choices" id="choices"></div>
|
| 142 |
+
</div>
|
| 143 |
+
<div class="msg" id="msg" style="display:none;"></div>
|
| 144 |
+
|
| 145 |
+
<div class="foot">
|
| 146 |
+
Tip: For multi-select questions, pick the exact set of answers (order doesn’t matter). Your state is saved to your browser via <span class="kbd">Save</span>/<span class="kbd">Load</span>.
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
</section>
|
| 150 |
+
|
| 151 |
+
<aside class="card">
|
| 152 |
+
<div class="hd">
|
| 153 |
+
<div><strong>Adventurer Sheet</strong></div>
|
| 154 |
+
<div class="pill" id="badgeMode">Mode: —</div>
|
| 155 |
+
</div>
|
| 156 |
+
<div class="bd">
|
| 157 |
+
<div class="stats">
|
| 158 |
+
<div class="stat">
|
| 159 |
+
<div class="k">HP</div>
|
| 160 |
+
<div class="v hearts" id="hp"></div>
|
| 161 |
+
</div>
|
| 162 |
+
<div class="stat">
|
| 163 |
+
<div class="k">Level</div>
|
| 164 |
+
<div class="v" id="lvl"></div>
|
| 165 |
+
</div>
|
| 166 |
+
<div class="stat">
|
| 167 |
+
<div class="k">XP</div>
|
| 168 |
+
<div class="v" id="xp"></div>
|
| 169 |
+
</div>
|
| 170 |
+
<div class="stat">
|
| 171 |
+
<div class="k">Gold</div>
|
| 172 |
+
<div class="v" id="gold"></div>
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
<h3 style="margin:14px 0 8px 0; font-size:14px;">Map</h3>
|
| 177 |
+
<div class="map" id="map"></div>
|
| 178 |
+
|
| 179 |
+
<h3 style="margin:14px 0 8px 0; font-size:14px;">Inventory</h3>
|
| 180 |
+
<div class="inventory" id="inv"></div>
|
| 181 |
+
</div>
|
| 182 |
+
</aside>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
<script>
|
| 187 |
+
const QUESTION_BANK = [{"id": "T1-Q1", "topic": 1, "qnum": 1, "stem": "You are using Microsoft 365 Copilot to enhance your productivity in Microsoft\nExcel.\nWhich two tasks can you achieve by using Copilot in Excel? Each correct answer\npresents a complete the solution.\nNOTE: Each correct selection is worth one point.", "options": [{"id": "A", "text": "Customize the conditional formatting rules for your data."}, {"id": "B", "text": "Insert a custom chart that has specific formatting."}, {"id": "C", "text": "Generate a summary of key insights from your data."}, {"id": "D", "text": "Build a pivot table based on your data."}], "answer": ["C", "D"], "multi": true}, {"id": "T1-Q2", "topic": 1, "qnum": 2, "stem": "You are using Microsoft 365 Copilot to enhance your productivity in Microsoft\nWord.\nWhich two tasks can you achieve by using Copilot in Word? Each correct answer\npresents a complete the solution.\nNOTE: Each correct selection is worth one point.", "options": [{"id": "A", "text": "Insert a table of contents based on the headings in your document."}, {"id": "B", "text": "Customize the page margins to match your company's document standards."}, {"id": "C", "text": "Generate a summary of the key points in your document."}, {"id": "D", "text": "Insert a custom watermark that has specific text and formatting."}], "answer": ["A", "C"], "multi": true}, {"id": "T1-Q3", "topic": 1, "qnum": 3, "stem": "You are using Microsoft 365 Copilot to enhance your productivity in Microsoft\nOutlook.\nWhich two tasks can you achieve by using Copilot in Outlook? Each correct\nanswer presents a complete the solution.\nNOTE: Each correct selection is worth one point.", "options": [{"id": "A", "text": "Insert a custom HTML signature that has specific formatting."}, {"id": "B", "text": "Generate a draft response to an email based on the context of the conversation."}, {"id": "C", "text": "Create a summary of unread emails in your inbox."}, {"id": "D", "text": "Customize the folder structure to organize your emails."}], "answer": ["B", "C"], "multi": true}, {"id": "T1-Q4", "topic": 1, "qnum": 4, "stem": "You have a Microsoft 365 Copilot license.\nYou are preparing a business presentation by using Copilot in PowerPoint.\nWhich two tasks can you achieve by using Copilot in PowerPoint? Each correct\nanswer presents a complete the solution.\nNOTE: Each correct selection is worth one point.", "options": [{"id": "A", "text": "Insert a custom animation for a specific object on a slide."}, {"id": "B", "text": "Create a slide layout based on the content of your presentation."}, {"id": "C", "text": "Customize the design theme of your presentation to match your company's branding."}, {"id": "D", "text": "Produce a summary slide based on the key points in your presentation."}], "answer": ["B", "D"], "multi": true}, {"id": "T1-Q5", "topic": 1, "qnum": 5, "stem": "You are using Microsoft 365 Copilot to enhance your productivity in Microsoft\nTeams.\nWhich two tasks can you achieve by using Copilot in Teams? Each correct answer\npresents a complete the solution.\nNOTE: Each correct selection is worth one point.", "options": [{"id": "A", "text": "Mute and unmute participants in a meeting based on their location."}, {"id": "B", "text": "Create a custom background and add the background to your video calls."}, {"id": "C", "text": "Create a meeting summary, including key decisions and action items."}, {"id": "D", "text": "Draft a response to a channel message based on the context of the conversation."}], "answer": ["C", "D"], "multi": true}, {"id": "T1-Q6", "topic": 1, "qnum": 6, "stem": "A colleague from another company shares a link to a prompt.\nWhen you select the link, you receive the following response: \"Prompt not found.\nSorry, it looks like the prompt is no longer available.\"\nWhat is a possible cause of the response?", "options": [{"id": "A", "text": "The prompt is outdated."}, {"id": "B", "text": "The prompt is outside of your organization."}, {"id": "C", "text": "The prompt contains a reference to a file that you do NOT have access to."}, {"id": "D", "text": "The prompt is a scheduled prompt."}, {"id": "E", "text": "The prompt contains a file that has a sensitivity label applied."}], "answer": ["B"], "multi": false}, {"id": "T1-Q7", "topic": 1, "qnum": 7, "stem": "You run a saved prompt and receive the following response: \"You asked for a\nsummary of File.docx. However, the file appears to be either empty, corrupted, or\nin a format that I cannot process.\"\nWhat is a possible cause of the response?", "options": [{"id": "A", "text": "You did NOT schedule the prompt to run."}, {"id": "B", "text": "You ran the prompt from a web app instead of a desktop app."}, {"id": "C", "text": "You used the wrong agent to run the prompt."}, {"id": "D", "text": "You do NOT have access to the file."}], "answer": ["D"], "multi": false}, {"id": "T1-Q8", "topic": 1, "qnum": 8, "stem": "You receive several images from a colleague.\nYou suspect that the images were generated by using Microsoft 365 Copilot.\nWhat can you use to verify whether the images were AI-generated?", "options": [{"id": "A", "text": "file names"}, {"id": "B", "text": "watermarks"}, {"id": "C", "text": "content credentials"}, {"id": "D", "text": "file descriptions"}], "answer": ["C"], "multi": false}, {"id": "T1-Q9", "topic": 1, "qnum": 9, "stem": "You are a merchandiser who is planning for the upcoming season.\nYou prompt Microsoft 365 Copilot to suggest which products to stock based on\nhistorical sales data. Without reviewing the suggestions or checking current\nmarket trends, you place a large order based solely on the output of Copilot.\nWhat is this an example of?", "options": [{"id": "A", "text": "verification"}, {"id": "B", "text": "overreliance"}, {"id": "C", "text": "fabrication"}, {"id": "D", "text": "prompt injection"}], "answer": ["B"], "multi": false}, {"id": "T1-Q10", "topic": 1, "qnum": 10, "stem": "You are comparing the difference between Microsoft 365 Copilot and Microsoft\n365 Copilot Chat.\nWhat is available in both versions?", "options": [{"id": "A", "text": "the Researcher agent"}, {"id": "B", "text": "Copilot Pages"}, {"id": "C", "text": "Copilot Notebooks"}], "answer": ["B"], "multi": false}, {"id": "T1-Q11", "topic": 1, "qnum": 11, "stem": "You are discussing Microsoft 365 Copilot with a colleague. The colleague asks\nwhich data Copilot uses to answer questions when using the Work scope.\nWhat should you tell your colleague?", "options": [{"id": "A", "text": "Copilot provides responses based only on data that the user can access and the general knowledge that Copilot was trained on."}, {"id": "B", "text": "Copilot provides responses based on all the data in your organization's Microsoft 365 environment and the general knowledge that Copilot was trained on."}, {"id": "C", "text": "Copilot provides responses based only on data that the user can access."}, {"id": "D", "text": "Copilot provides responses based only on the general knowledge that Copilot was trained on."}], "answer": ["A"], "multi": false}, {"id": "T1-Q18", "topic": 1, "qnum": 18, "stem": "You open Microsoft 356 Copilot as shown in the following exhibit.\nWhat can you infer about the Standings conversation?", "options": [{"id": "A", "text": "The conversation contains only anonymous data."}, {"id": "B", "text": "The conversation contains a verified response."}, {"id": "C", "text": "The conversation was added to a notebook."}, {"id": "D", "text": "The conversation contains a scheduled prompt."}, {"id": "E", "text": "The conversation was shared."}], "answer": ["A"], "multi": false}, {"id": "T2-Q2", "topic": 2, "qnum": 2, "stem": "You are using Microsoft 365 Copilot to plan a trip and have asked Copilot to\nremember several details about the trip.\nYou cancel the trip plans.\nYou need to ensure that Copilot forgets the details.\nWhat should you do?", "options": [{"id": "A", "text": "From the My Account portal, delete the Copilot activity history."}, {"id": "B", "text": "From Copilot, delete memories."}, {"id": "C", "text": "From Copilot, delete custom instructions."}, {"id": "D", "text": "From Copilot, delete all the conversation about the trip."}], "answer": ["B"], "multi": false}, {"id": "T2-Q3", "topic": 2, "qnum": 3, "stem": "You are a marketing assistant preparing for a budget meeting with your manager.\nYou need to evaluate key spending and performance trends from the last year to\nunderstand which marketing channels deliver the best return on investment\n(ROI).\nWhat should you use in Microsoft 365 Copilot to achieve the goal? More than one\nanswer choice may achieve the goal. Select the BEST answer.", "options": [{"id": "A", "text": "the Analyst agent"}, {"id": "B", "text": "Chat"}, {"id": "C", "text": "the Researcher agent"}, {"id": "D", "text": "a notebook"}], "answer": ["A"], "multi": false}, {"id": "T2-Q4", "topic": 2, "qnum": 4, "stem": "You have several pages in Microsoft 365 Copilot related to the research you are\ndoing for a new product.\nYou need to simplify your ability to have many different Copilot conversations\nabout the pages.\nWhat should you do first?", "options": [{"id": "A", "text": "Create a story."}, {"id": "B", "text": "Add the pages to a notebook."}, {"id": "C", "text": "Add the pages to a conversation."}, {"id": "D", "text": "Download the pages locally."}], "answer": ["B"], "multi": false}, {"id": "T2-Q5", "topic": 2, "qnum": 5, "stem": "You are creating a custom analytics agent in the Microsoft 365 Copilot app. The\nagent will use Microsoft Excel files that contain sales data as knowledge.\nYou need to ensure that the agent can create visualizations, perform\nmathematical operations, create aggregations, and analyze the data in the files.\nWhat should you add to the agent?", "options": [{"id": "A", "text": "a suggested prompt"}, {"id": "B", "text": "code interpreter"}, {"id": "C", "text": "image generator"}, {"id": "D", "text": "a template"}], "answer": ["A"], "multi": false}, {"id": "T2-Q6", "topic": 2, "qnum": 6, "stem": "You create a prompt in Microsoft 365 Copilot to help you create a draft project\nreport.\nYou need the prompt to be available in the Copilot Prompt Gallery.\nWhat should you do first?", "options": [{"id": "A", "text": "Create a notebook."}, {"id": "B", "text": "Add an agent."}, {"id": "C", "text": "Create a page."}, {"id": "D", "text": "Run the prompt."}], "answer": ["D"], "multi": false}, {"id": "T2-Q7", "topic": 2, "qnum": 7, "stem": "You create a Microsoft 365 Copilot notebook and add a file named Process.docx\nfrom a local folder.\nYesterday, you updated Process.docx in the local folder.\nWhat will occur when you chat in the notebook?", "options": [{"id": "A", "text": "The chat will reference the most recent version of Process.docx."}, {"id": "B", "text": "The chat will reference both versions of Process.docx."}, {"id": "C", "text": "The chat will reference only the original version of Process.docx."}], "answer": ["C"], "multi": false}, {"id": "T2-Q8", "topic": 2, "qnum": 8, "stem": "You have several separate Microsoft 365 Copilot conversations.\nYou need to gather the conversations so that you can find them more easily, and\nthe conversations can share the same reference material.\nWhat is the best solution to achieve the goal? More than one answer choice may\nachieve the goal. Select the BEST answer.", "options": [{"id": "A", "text": "a notebook"}, {"id": "B", "text": "an agent"}, {"id": "C", "text": "an app"}, {"id": "D", "text": "a page"}], "answer": ["A"], "multi": false}, {"id": "T2-Q9", "topic": 2, "qnum": 9, "stem": "You plan to use a notebook in Microsoft 365 Copilot.\nWhat is the purpose of a notebook?", "options": [{"id": "A", "text": "to create a transcript of a conversation that you can easily email to other users"}, {"id": "B", "text": "to provide a private location that can be used to organize reference materials across related conversations"}, {"id": "C", "text": "to generate a summary of all the interactions you had with Copilot during a specific conversation"}], "answer": ["B"], "multi": false}, {"id": "T2-Q10", "topic": 2, "qnum": 10, "stem": "You have a Microsoft Excel workbook open that contains a table. The table\ncontains sales data and the following columns:\n• OrderDate\n• OrderNumber\n• Amount\nYou plan to use Microsoft Copilot in Excel to generate a monthly summary of the\nsales data in the table.\nWhat should you include in the prompt to generate an effective summary? More\nthan one answer choice may achieve the goal. Select the BEST answer.", "options": [{"id": "A", "text": "the desired format of the response"}, {"id": "B", "text": "a specific cell range from the worksheet"}, {"id": "C", "text": "the file name of the workbook"}, {"id": "D", "text": "the clear goal of the summary"}, {"id": "E", "text": "a formula to calculate monthly totals"}], "answer": ["D"], "multi": false}, {"id": "T2-Q11", "topic": 2, "qnum": 11, "stem": "You are creating a prompt in Microsoft 365 Copilot to get information about a\nproposal.\nYou need to ensure that the response is grounded in the proposal's information.\nWhat is the best approach to achieve the goal? More than one answer choice may\nachieve the goal. Select the BEST answer.", "options": [{"id": "A", "text": "Add context about the intended audience."}, {"id": "B", "text": "Instruct Copilot to rely on its training data to infer proposal details."}, {"id": "C", "text": "Reference the proposal content in the prompt."}, {"id": "D", "text": "Add a specific goal that you want Copilot to accomplish."}], "answer": ["C"], "multi": false}, {"id": "T2-Q12", "topic": 2, "qnum": 12, "stem": "You need to improve prompt-grounding during a Microsoft 365 Copilot\nconversation.\nWhat is the best approach to achieve the goal? More than one answer choice may\nachieve the goal. Select the BEST answer.", "options": [{"id": "A", "text": "Use shorter prompts."}, {"id": "B", "text": "Provide the desired output format."}, {"id": "C", "text": "Avoid technical jargon."}, {"id": "D", "text": "Reference specific files."}], "answer": ["D"], "multi": false}, {"id": "T2-Q13", "topic": 2, "qnum": 13, "stem": "You create a Microsoft 365 Copilot notebook and add a file named Process.docx\nfrom a Microsoft SharePoint site as a reference.\nYesterday, a user updated Process.docx in the SharePoint site.\nWhat will occur when you chat in the notebook?", "options": [{"id": "A", "text": "The chat will reference only the original version of Process.docx."}, {"id": "B", "text": "The chat will reference the most recent version of Process.docx."}, {"id": "C", "text": "The chat will reference both versions of Process.docx."}], "answer": ["B"], "multi": false}, {"id": "T2-Q14", "topic": 2, "qnum": 14, "stem": "You use Microsoft 365 Copilot.\nYou discover that you had a conversation that used a knowledge source that\ncontains confidential information.\nYou need to delete the conversation data without requiring administrative\napproval. You must retain your other conversations, if possible.\nWhat should you use?", "options": [{"id": "A", "text": "the Microsoft 365 admin center"}, {"id": "B", "text": "the My Account portal in Microsoft 365 Copilot"}, {"id": "C", "text": "the Microsoft Purview compliance portal"}, {"id": "D", "text": "the Microsoft 365 Copilot app"}], "answer": ["B"], "multi": false}, {"id": "T2-Q15", "topic": 2, "qnum": 15, "stem": "You have an open Microsoft 365 Copilot conversation.\nYou need to chat with an agent named Agent1 during the conversation.\nWhat should you enter to call Agent1?", "options": [{"id": "A", "text": "[Agent1]"}, {"id": "B", "text": "#Agent1"}, {"id": "C", "text": "/Agent1"}, {"id": "D", "text": "@Agent1"}], "answer": ["D"], "multi": false}, {"id": "T2-Q16", "topic": 2, "qnum": 16, "stem": "You use Microsoft 365 Copilot.\nYou regularly upload the same five files to Copilot chats.\nYou need to simplify referencing the files in the chats.\nWhat are two ways to achieve the goal? Each correct answer presents a complete\nsolution.\nNOTE: Each correct selection is worth one point.", "options": [{"id": "A", "text": "Create an agent that has a knowledge source."}, {"id": "B", "text": "Send a prompt that includes the files, and then save the prompt."}, {"id": "C", "text": "Create a notebook that references the files."}, {"id": "D", "text": "Ask the data engineers to create a fine-tuned model."}, {"id": "E", "text": "Zip the documents and upload the ZIP file to the chats."}], "answer": ["B", "C"], "multi": true}, {"id": "T2-Q17", "topic": 2, "qnum": 17, "stem": "You have a Microsoft 365 subscription.\nYou do NOT have a Microsoft 365 Copilot license.\nWhen you send a prompt in Microsoft 365 Copilot Chat, which three sources does\nCopilot Chat use to respond? Each correct answer presents part of the solution.\nNOTE: Each correct selection is worth one point.", "options": [{"id": "A", "text": "chat data in Microsoft Teams"}, {"id": "B", "text": "your organization's databases"}, {"id": "C", "text": "the prompt context"}, {"id": "D", "text": "organizational data in Microsoft 365"}, {"id": "E", "text": "internet search data"}, {"id": "F", "text": "the conversation history"}], "answer": ["C", "E", "F"], "multi": true}, {"id": "T2-Q18", "topic": 2, "qnum": 18, "stem": "You are a project coordinator for a small consulting firm.\nYou are responsible for tracking client communications, managing project\ntimelines, and preparing weekly status updates for internal stakeholders.\nYou have a Microsoft 365 Copilot license.\nYou create an agent to help you monitor project milestones, follow up on client\nemails, and generate weekly summary reports.\nWith whom can you share the agent?", "options": [{"id": "A", "text": "only the people in your organization"}, {"id": "B", "text": "the people in your organization and people that have personal Microsoft accounts"}, {"id": "C", "text": "only Microsoft Teams channel members"}, {"id": "D", "text": "any person who has a valid email address"}], "answer": ["A"], "multi": false}, {"id": "T3-Q3", "topic": 3, "qnum": 3, "stem": "You plan to summarize a proposal for a partnership between your company and\nanother company.\nYou need to use Microsoft 365 Copilot to summarize the information in the\nproposal as a bulleted list of three to five key points for a project kickoff meeting.\nEach bullet point must be concise and simply worded.\nWhich two details should you include in the prompt? Each correct answer\npresents part of the solution.\nNOTE: Each correct selection is worth one point.", "options": [{"id": "A", "text": "a list of contact details for key stakeholders of the partnership"}, {"id": "B", "text": "profiles of both companies generated by using the Researcher agent"}, {"id": "C", "text": "a specific knowledge source to ground the response"}, {"id": "D", "text": "concise instructions for what to do with the proposal"}], "answer": ["B", "C"], "multi": true}, {"id": "T3-Q4", "topic": 3, "qnum": 4, "stem": "In a Microsoft 365 Copilot conversation, you generate a report, and then edit the\nreport in a page.\nYou need to collaborate with a colleague on the report.\nWhat are two ways to achieve the goal? Each correct answer presents a complete\nthe solution.\nNOTE: Each correct selection is worth one point.", "options": [{"id": "A", "text": "Mention the colleague in the page."}, {"id": "B", "text": "Open the page in Microsoft Word."}, {"id": "C", "text": "Share the page link."}, {"id": "D", "text": "Add the page to a notebook."}], "answer": ["B", "C"], "multi": true}, {"id": "T3-Q5", "topic": 3, "qnum": 5, "stem": "You attend many Microsoft Teams meetings.\nYou want to ensure that you can catch up on key points and next steps after each\nmeeting.\nWhich Microsoft 365 Copilot feature should you use after the meetings?", "options": [{"id": "A", "text": "Recap"}, {"id": "B", "text": "Meeting Whiteboard"}, {"id": "C", "text": "Meeting info"}, {"id": "D", "text": "Thread summaries"}], "answer": ["A"], "multi": false}, {"id": "T3-Q6", "topic": 3, "qnum": 6, "stem": "You are creating a custom agent in the Microsoft 365 Copilot app for the\nmarketing team at your company. The agent will be used to produce marketing\ncollateral, including copy, logos and artwork.\nWhat should you add to the agent? More than one answer choice may achieve the\ngoal. Select the BEST answer.", "options": [{"id": "A", "text": "code interpreter"}, {"id": "B", "text": "image generator"}, {"id": "C", "text": "a suggested prompt"}, {"id": "D", "text": "a template"}], "answer": ["B"], "multi": false}, {"id": "T3-Q7", "topic": 3, "qnum": 7, "stem": "You use Microsoft 365 Copilot to create a proposal in Microsoft Word based on a\ndocument in a Microsoft SharePoint library.\nYou need to ensure that the information cited in the proposal is accurate.\nWhat is the best approach to achieve the goal? More than one answer choice may\nachieve the goal. Select the BEST answer.", "options": [{"id": "A", "text": "Manually verify the citations in the generated content."}, {"id": "B", "text": "Use Copilot in Word to verify the citations."}, {"id": "C", "text": "Create an agent that contains your organization's editorial standards and use the agent to verify the citations."}, {"id": "D", "text": "Create a notebook that contains all the reference material and use the notebook to verify the citations."}], "answer": ["B"], "multi": false}, {"id": "T3-Q9", "topic": 3, "qnum": 9, "stem": "You sign in to the Microsoft 365 Copilot app by using your work account as shown\nin the following exhibit.\nA colleague tells you that when they open the Microsoft 365 Copilot app, they\nhave access to the Researcher agent.\nYou need to access to the Researcher agent.\nWhat should you do?", "options": [{"id": "A", "text": "Request a Microsoft 365 Copilot license from an administrator."}, {"id": "B", "text": "Select Explore agents and then search for Researcher."}, {"id": "C", "text": "Sign in to the Copilot app by using a personal account."}, {"id": "D", "text": "From Microsoft Edge, use your work account to sign in to https://copilot.microsoft.com."}], "answer": ["A"], "multi": false}];
|
| 188 |
+
|
| 189 |
+
const REGIONS = [
|
| 190 |
+
{
|
| 191 |
+
id: 1,
|
| 192 |
+
title: "The App Isles (Topic 1)",
|
| 193 |
+
desc: "Excel Caverns, Word Citadel, Outlook Docks, PowerPoint Peaks, Teams Tavern… prove you can wield Copilot across Microsoft 365 apps.",
|
| 194 |
+
reward: "Badge of Productivity"
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
id: 2,
|
| 198 |
+
title: "The Notebook Labyrinth (Topic 2)",
|
| 199 |
+
desc: "Notebooks, agents, grounding, prompts, and sharing rules. Keep your answers anchored or the labyrinth loops forever.",
|
| 200 |
+
reward: "Grounding Compass"
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
id: 3,
|
| 204 |
+
title: "The Content Forge (Topic 3)",
|
| 205 |
+
desc: "Draft, analyze, cite, and collaborate. Beware: the Citation Wraith punishes lazy verification.",
|
| 206 |
+
reward: "Quill of Clarity"
|
| 207 |
+
},
|
| 208 |
+
];
|
| 209 |
+
|
| 210 |
+
// ---------- helpers ----------
|
| 211 |
+
const $ = (id) => document.getElementById(id);
|
| 212 |
+
const clamp = (n, a, b) => Math.max(a, Math.min(b, n));
|
| 213 |
+
const sortLetters = (arr) => [...arr].sort();
|
| 214 |
+
const eqSet = (a,b) => JSON.stringify(sortLetters(a)) === JSON.stringify(sortLetters(b));
|
| 215 |
+
const nowIso = () => new Date().toISOString();
|
| 216 |
+
// ---------- audio (offline SFX via Web Audio) ----------
|
| 217 |
+
let audioCtx = null;
|
| 218 |
+
let sfxEnabled = true;
|
| 219 |
+
const SFX_KEY = "copilot-quest-sfx-enabled-v1";
|
| 220 |
+
|
| 221 |
+
function loadSfxPref() {
|
| 222 |
+
try {
|
| 223 |
+
const v = localStorage.getItem(SFX_KEY);
|
| 224 |
+
if (v === "0" || v === "1") sfxEnabled = (v === "1");
|
| 225 |
+
} catch (e) {}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
function saveSfxPref() {
|
| 229 |
+
try { localStorage.setItem(SFX_KEY, sfxEnabled ? "1" : "0"); } catch (e) {}
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
function setSoundButton() {
|
| 233 |
+
const b = $("btnSound");
|
| 234 |
+
if (!b) return;
|
| 235 |
+
const supported = !!(window.AudioContext || window.webkitAudioContext);
|
| 236 |
+
if (!supported) {
|
| 237 |
+
b.textContent = "Sound: Unsupported";
|
| 238 |
+
b.disabled = true;
|
| 239 |
+
return;
|
| 240 |
+
}
|
| 241 |
+
b.textContent = sfxEnabled ? "Sound: On" : "Sound: Off";
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
function initAudio() {
|
| 245 |
+
if (!sfxEnabled) return;
|
| 246 |
+
if (!audioCtx) {
|
| 247 |
+
const AC = window.AudioContext || window.webkitAudioContext;
|
| 248 |
+
if (!AC) return;
|
| 249 |
+
audioCtx = new AC();
|
| 250 |
+
}
|
| 251 |
+
if (audioCtx.state === "suspended") audioCtx.resume();
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
function playTone(freq, duration = 0.07, type = "sine", gain = 0.05, when = 0) {
|
| 255 |
+
if (!sfxEnabled) return;
|
| 256 |
+
initAudio();
|
| 257 |
+
if (!audioCtx) return;
|
| 258 |
+
|
| 259 |
+
const t0 = audioCtx.currentTime + when;
|
| 260 |
+
const osc = audioCtx.createOscillator();
|
| 261 |
+
const g = audioCtx.createGain();
|
| 262 |
+
|
| 263 |
+
osc.type = type;
|
| 264 |
+
osc.frequency.setValueAtTime(freq, t0);
|
| 265 |
+
|
| 266 |
+
// Simple "blip" envelope
|
| 267 |
+
g.gain.setValueAtTime(0.0001, t0);
|
| 268 |
+
g.gain.exponentialRampToValueAtTime(gain, t0 + 0.01);
|
| 269 |
+
g.gain.exponentialRampToValueAtTime(0.0001, t0 + duration);
|
| 270 |
+
|
| 271 |
+
osc.connect(g);
|
| 272 |
+
g.connect(audioCtx.destination);
|
| 273 |
+
|
| 274 |
+
osc.start(t0);
|
| 275 |
+
osc.stop(t0 + duration + 0.02);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
function playPattern(notes) {
|
| 279 |
+
// notes: [{ f:Hz, d:sec, w:sec, t:type, g:gain }]
|
| 280 |
+
let offset = 0;
|
| 281 |
+
notes.forEach(n => {
|
| 282 |
+
playTone(n.f, n.d ?? 0.07, n.t ?? "sine", n.g ?? 0.05, offset);
|
| 283 |
+
offset += (n.w ?? n.d ?? 0.07);
|
| 284 |
+
});
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
function playSfx(name) {
|
| 288 |
+
switch (name) {
|
| 289 |
+
case "click":
|
| 290 |
+
playTone(520, 0.03, "triangle", 0.035);
|
| 291 |
+
break;
|
| 292 |
+
case "start":
|
| 293 |
+
playPattern([
|
| 294 |
+
{ f: 440, d: 0.05, w: 0.05, t: "triangle", g: 0.05 },
|
| 295 |
+
{ f: 660, d: 0.06, w: 0.06, t: "triangle", g: 0.05 },
|
| 296 |
+
{ f: 880, d: 0.08, w: 0.08, t: "sine", g: 0.055 },
|
| 297 |
+
]);
|
| 298 |
+
break;
|
| 299 |
+
case "enter":
|
| 300 |
+
playPattern([
|
| 301 |
+
{ f: 523, d: 0.05, w: 0.05, t: "triangle", g: 0.05 },
|
| 302 |
+
{ f: 784, d: 0.07, w: 0.07, t: "triangle", g: 0.05 },
|
| 303 |
+
]);
|
| 304 |
+
break;
|
| 305 |
+
case "correct":
|
| 306 |
+
playPattern([
|
| 307 |
+
{ f: 784, d: 0.06, w: 0.06, t: "sine", g: 0.055 },
|
| 308 |
+
{ f: 988, d: 0.06, w: 0.06, t: "sine", g: 0.055 },
|
| 309 |
+
{ f: 1175, d: 0.08, w: 0.08, t: "sine", g: 0.060 },
|
| 310 |
+
]);
|
| 311 |
+
break;
|
| 312 |
+
case "wrong":
|
| 313 |
+
playPattern([
|
| 314 |
+
{ f: 220, d: 0.08, w: 0.08, t: "sawtooth", g: 0.040 },
|
| 315 |
+
{ f: 180, d: 0.08, w: 0.08, t: "sawtooth", g: 0.040 },
|
| 316 |
+
{ f: 140, d: 0.10, w: 0.10, t: "sawtooth", g: 0.040 },
|
| 317 |
+
]);
|
| 318 |
+
break;
|
| 319 |
+
case "relic":
|
| 320 |
+
playPattern([
|
| 321 |
+
{ f: 880, d: 0.04, w: 0.04, t: "triangle", g: 0.050 },
|
| 322 |
+
{ f: 1320, d: 0.05, w: 0.05, t: "triangle", g: 0.050 },
|
| 323 |
+
]);
|
| 324 |
+
break;
|
| 325 |
+
case "region":
|
| 326 |
+
playPattern([
|
| 327 |
+
{ f: 523, d: 0.07, w: 0.07, t: "triangle", g: 0.050 },
|
| 328 |
+
{ f: 659, d: 0.07, w: 0.07, t: "triangle", g: 0.050 },
|
| 329 |
+
{ f: 784, d: 0.09, w: 0.09, t: "sine", g: 0.055 },
|
| 330 |
+
]);
|
| 331 |
+
break;
|
| 332 |
+
case "victory":
|
| 333 |
+
playPattern([
|
| 334 |
+
{ f: 659, d: 0.07, w: 0.07, t: "triangle", g: 0.050 },
|
| 335 |
+
{ f: 784, d: 0.07, w: 0.07, t: "triangle", g: 0.050 },
|
| 336 |
+
{ f: 988, d: 0.10, w: 0.10, t: "sine", g: 0.060 },
|
| 337 |
+
{ f: 1319, d: 0.14, w: 0.14, t: "sine", g: 0.060 },
|
| 338 |
+
]);
|
| 339 |
+
break;
|
| 340 |
+
case "gameover":
|
| 341 |
+
playPattern([
|
| 342 |
+
{ f: 196, d: 0.10, w: 0.10, t: "sawtooth", g: 0.040 },
|
| 343 |
+
{ f: 165, d: 0.12, w: 0.12, t: "sawtooth", g: 0.040 },
|
| 344 |
+
{ f: 131, d: 0.16, w: 0.16, t: "sawtooth", g: 0.040 },
|
| 345 |
+
]);
|
| 346 |
+
break;
|
| 347 |
+
case "save":
|
| 348 |
+
playPattern([
|
| 349 |
+
{ f: 660, d: 0.04, w: 0.04, t: "triangle", g: 0.040 },
|
| 350 |
+
{ f: 880, d: 0.05, w: 0.05, t: "triangle", g: 0.040 },
|
| 351 |
+
]);
|
| 352 |
+
break;
|
| 353 |
+
case "load":
|
| 354 |
+
playPattern([
|
| 355 |
+
{ f: 880, d: 0.04, w: 0.04, t: "triangle", g: 0.040 },
|
| 356 |
+
{ f: 660, d: 0.05, w: 0.05, t: "triangle", g: 0.040 },
|
| 357 |
+
]);
|
| 358 |
+
break;
|
| 359 |
+
case "reset":
|
| 360 |
+
playPattern([
|
| 361 |
+
{ f: 330, d: 0.06, w: 0.06, t: "triangle", g: 0.040 },
|
| 362 |
+
{ f: 220, d: 0.08, w: 0.08, t: "triangle", g: 0.040 },
|
| 363 |
+
]);
|
| 364 |
+
break;
|
| 365 |
+
default:
|
| 366 |
+
break;
|
| 367 |
+
}
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
// ---------- background music (embedded MP3; offline) ----------
|
| 371 |
+
let bgm = null;
|
| 372 |
+
let bgmEnabled = true; // default ON (toggle with button)
|
| 373 |
+
let bgmVolume = 0.25;
|
| 374 |
+
const BGM_KEY = "copilot-quest-bgm-enabled-v1";
|
| 375 |
+
const BGM_VOL_KEY = "copilot-quest-bgm-vol-v1";
|
| 376 |
+
const BGM_SRC = ''; // Music removed to reduce file size. You can add an external MP3 URL here if needed.
|
| 377 |
+
|
| 378 |
+
function initBgm() {
|
| 379 |
+
if (!bgm) {
|
| 380 |
+
bgm = new Audio(BGM_SRC);
|
| 381 |
+
bgm.loop = true;
|
| 382 |
+
bgm.preload = "auto";
|
| 383 |
+
}
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
function loadBgmPref() {
|
| 387 |
+
try {
|
| 388 |
+
const e = localStorage.getItem(BGM_KEY);
|
| 389 |
+
if (e === "0" || e === "1") bgmEnabled = (e === "1");
|
| 390 |
+
const v = localStorage.getItem(BGM_VOL_KEY);
|
| 391 |
+
if (v !== null && v !== undefined && v !== "") {
|
| 392 |
+
const n = Number(v);
|
| 393 |
+
if (!Number.isNaN(n)) bgmVolume = clamp(n, 0, 1);
|
| 394 |
+
}
|
| 395 |
+
} catch (e) {}
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
function saveBgmPref() {
|
| 399 |
+
try {
|
| 400 |
+
localStorage.setItem(BGM_KEY, bgmEnabled ? "1" : "0");
|
| 401 |
+
localStorage.setItem(BGM_VOL_KEY, String(bgmVolume));
|
| 402 |
+
} catch (e) {}
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
function setMusicUi() {
|
| 406 |
+
const b = $("btnMusic");
|
| 407 |
+
const s = $("musicVol");
|
| 408 |
+
if (b) b.textContent = bgmEnabled ? "Music: On" : "Music: Off";
|
| 409 |
+
if (s) s.value = String(Math.round(bgmVolume * 100));
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
async function tryPlayBgm() {
|
| 413 |
+
if (!bgmEnabled) return;
|
| 414 |
+
initBgm();
|
| 415 |
+
if (!bgm) return;
|
| 416 |
+
|
| 417 |
+
bgm.volume = clamp(bgmVolume, 0, 1);
|
| 418 |
+
|
| 419 |
+
try {
|
| 420 |
+
// Must be called during/after a user gesture in most browsers.
|
| 421 |
+
await bgm.play();
|
| 422 |
+
} catch (e) {
|
| 423 |
+
// Ignore autoplay blocks; we'll try again on the next user gesture.
|
| 424 |
+
}
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
function pauseBgm() {
|
| 428 |
+
if (bgm) bgm.pause();
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
// Try to start music on the first user gesture (Start button click, etc.)
|
| 432 |
+
document.addEventListener("pointerdown", () => {
|
| 433 |
+
// resume web-audio sfx if it exists & was suspended
|
| 434 |
+
if (audioCtx && audioCtx.state === "suspended") audioCtx.resume();
|
| 435 |
+
tryPlayBgm();
|
| 436 |
+
}, { once: true });
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
function regionIntroText(region) {
|
| 442 |
+
return [
|
| 443 |
+
"🧭 Welcome, Adventurer.",
|
| 444 |
+
"",
|
| 445 |
+
`You’ve stepped into ${region.title}.`,
|
| 446 |
+
region.desc,
|
| 447 |
+
"",
|
| 448 |
+
"Rule of the realm: answer challenges to gain XP + gold + relics. Miss too many, and your HP hits zero…",
|
| 449 |
+
"",
|
| 450 |
+
"Press “Enter Region” when you’re ready."
|
| 451 |
+
].join("\n");
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
function flavorFor(question) {
|
| 455 |
+
const t = question.topic;
|
| 456 |
+
if (t === 1) return "A gatekeeper blocks the path. It speaks in app-terms and awaits your choice.";
|
| 457 |
+
if (t === 2) return "The corridor walls are made of sticky notes and notebooks. Choose wisely or loop back.";
|
| 458 |
+
return "The forge roars. A scroll demands precision. A wrong answer smudges the ink of fate.";
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
function relicFor(question) {
|
| 462 |
+
const base = question.topic === 1 ? "App Rune" : question.topic === 2 ? "Notebook Sigil" : "Forge Charm";
|
| 463 |
+
return {
|
| 464 |
+
name: `${base} ${question.topic}-${question.qnum}`,
|
| 465 |
+
desc: question.multi
|
| 466 |
+
? "A relic that rewards exact multi-select mastery."
|
| 467 |
+
: "A relic that rewards decisive single-choice clarity."
|
| 468 |
+
};
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
// ---------- state ----------
|
| 472 |
+
const DEFAULT = {
|
| 473 |
+
ver: 1,
|
| 474 |
+
mode: "title", // title | intro | question | feedback | regionDone | finished | gameOver
|
| 475 |
+
regionId: 1,
|
| 476 |
+
order: [],
|
| 477 |
+
pos: 0,
|
| 478 |
+
hp: 3,
|
| 479 |
+
xp: 0,
|
| 480 |
+
gold: 0,
|
| 481 |
+
lvl: 1,
|
| 482 |
+
inventory: [],
|
| 483 |
+
last: null,
|
| 484 |
+
startedAt: null,
|
| 485 |
+
};
|
| 486 |
+
|
| 487 |
+
let state = structuredClone(DEFAULT);
|
| 488 |
+
|
| 489 |
+
function computeLevel(xp) {
|
| 490 |
+
return 1 + Math.floor(Math.sqrt(xp / 40));
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
function regionQuestions(regionId) {
|
| 494 |
+
return QUESTION_BANK.filter(q => q.topic === regionId);
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
function buildRunOrder() {
|
| 498 |
+
function shuffle(arr) {
|
| 499 |
+
const a = [...arr];
|
| 500 |
+
for (let i = a.length - 1; i > 0; i--) {
|
| 501 |
+
const j = Math.floor(Math.random() * (i + 1));
|
| 502 |
+
[a[i], a[j]] = [a[j], a[i]];
|
| 503 |
+
}
|
| 504 |
+
return a;
|
| 505 |
+
}
|
| 506 |
+
const byRegion = REGIONS.map(r => shuffle(regionQuestions(r.id)));
|
| 507 |
+
return byRegion.flat().map(q => q.id);
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
function getCurrentQuestion() {
|
| 511 |
+
const qid = state.order[state.pos];
|
| 512 |
+
return QUESTION_BANK.find(q => q.id === qid);
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
function regionProgress() {
|
| 516 |
+
const regQ = regionQuestions(state.regionId);
|
| 517 |
+
const completedInRegion = regQ.filter(q => {
|
| 518 |
+
const idx = state.order.indexOf(q.id);
|
| 519 |
+
return idx !== -1 && idx < state.pos;
|
| 520 |
+
}).length;
|
| 521 |
+
return { done: completedInRegion, total: regQ.length };
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
function fullProgress() {
|
| 525 |
+
return { done: clamp(state.pos, 0, state.order.length), total: state.order.length };
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
// ---------- rendering ----------
|
| 529 |
+
function renderMap() {
|
| 530 |
+
const map = $("map");
|
| 531 |
+
map.innerHTML = "";
|
| 532 |
+
REGIONS.forEach(r => {
|
| 533 |
+
const div = document.createElement("div");
|
| 534 |
+
div.className = "node";
|
| 535 |
+
if (r.id < state.regionId) div.classList.add("done");
|
| 536 |
+
if (r.id === state.regionId) div.classList.add("active");
|
| 537 |
+
const rQs = regionQuestions(r.id);
|
| 538 |
+
const done = rQs.filter(q => {
|
| 539 |
+
const idx = state.order.indexOf(q.id);
|
| 540 |
+
return idx !== -1 && idx < state.pos;
|
| 541 |
+
}).length;
|
| 542 |
+
div.innerHTML = `<div class="t">${r.title}</div>
|
| 543 |
+
<div class="d">${r.desc}</div>
|
| 544 |
+
<div class="d" style="margin-top:6px;">Progress: <span class="kbd">${done}/${rQs.length}</span></div>`;
|
| 545 |
+
map.appendChild(div);
|
| 546 |
+
});
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
function renderStats() {
|
| 550 |
+
$("hp").textContent = "♥".repeat(state.hp) + "·".repeat(Math.max(0, 3 - state.hp));
|
| 551 |
+
$("xp").textContent = String(state.xp);
|
| 552 |
+
$("gold").textContent = String(state.gold);
|
| 553 |
+
$("lvl").textContent = String(state.lvl);
|
| 554 |
+
$("badgeMode").textContent = `Mode: ${state.mode}`;
|
| 555 |
+
const reg = REGIONS.find(r => r.id === state.regionId);
|
| 556 |
+
$("badgeRegion").textContent = `Region: ${reg ? reg.title : "—"}`;
|
| 557 |
+
const rp = regionProgress();
|
| 558 |
+
const fp = fullProgress();
|
| 559 |
+
$("badgeProgress").textContent = `Progress: ${rp.done}/${rp.total} (Total ${fp.done}/${fp.total})`;
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
function renderInventory() {
|
| 563 |
+
const inv = $("inv");
|
| 564 |
+
inv.innerHTML = "";
|
| 565 |
+
if (!state.inventory.length) {
|
| 566 |
+
const empty = document.createElement("div");
|
| 567 |
+
empty.className = "item";
|
| 568 |
+
empty.innerHTML = `<div class="name">Inventory empty</div>
|
| 569 |
+
<div class="desc">Win encounters to collect relics.</div>`;
|
| 570 |
+
inv.appendChild(empty);
|
| 571 |
+
return;
|
| 572 |
+
}
|
| 573 |
+
state.inventory.slice().reverse().slice(0, 8).forEach(it => {
|
| 574 |
+
const div = document.createElement("div");
|
| 575 |
+
div.className = "item";
|
| 576 |
+
div.innerHTML = `<div class="name">${it.name}</div><div class="desc">${it.desc}</div>`;
|
| 577 |
+
inv.appendChild(div);
|
| 578 |
+
});
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
function setMsg(kind, text) {
|
| 582 |
+
const box = $("msg");
|
| 583 |
+
box.style.display = "block";
|
| 584 |
+
box.className = `msg ${kind || ""}`;
|
| 585 |
+
box.textContent = text;
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
function hideMsg() {
|
| 589 |
+
const box = $("msg");
|
| 590 |
+
box.style.display = "none";
|
| 591 |
+
box.textContent = "";
|
| 592 |
+
box.className = "msg";
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
function showQuestion(q) {
|
| 596 |
+
$("qArea").style.display = "block";
|
| 597 |
+
$("qText").textContent = q.stem;
|
| 598 |
+
$("qHint").textContent = q.multi ? "Select ALL correct answers (multi-select)." : "Select ONE answer.";
|
| 599 |
+
const choices = $("choices");
|
| 600 |
+
choices.innerHTML = "";
|
| 601 |
+
|
| 602 |
+
const inputType = q.multi ? "checkbox" : "radio";
|
| 603 |
+
q.options.forEach(opt => {
|
| 604 |
+
const id = `opt-${q.id}-${opt.id}`;
|
| 605 |
+
const lab = document.createElement("label");
|
| 606 |
+
lab.className = "choice";
|
| 607 |
+
lab.setAttribute("for", id);
|
| 608 |
+
lab.innerHTML = `
|
| 609 |
+
<div class="letter">${opt.id}</div>
|
| 610 |
+
<div style="display:flex; gap:10px; align-items:flex-start; width:100%;">
|
| 611 |
+
<input id="${id}" name="answer" type="${inputType}" value="${opt.id}" style="margin-top:6px;">
|
| 612 |
+
<div class="txt">${opt.text}</div>
|
| 613 |
+
</div>
|
| 614 |
+
`;
|
| 615 |
+
choices.appendChild(lab);
|
| 616 |
+
});
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
function getSelected() {
|
| 620 |
+
const inputs = Array.from(document.querySelectorAll("input[name='answer']"));
|
| 621 |
+
return inputs.filter(i => i.checked).map(i => i.value);
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
function clearSelected() {
|
| 625 |
+
Array.from(document.querySelectorAll("input[name='answer']")).forEach(i => { i.checked = false; i.disabled = false; });
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
function render() {
|
| 629 |
+
renderStats();
|
| 630 |
+
renderMap();
|
| 631 |
+
renderInventory();
|
| 632 |
+
|
| 633 |
+
const primary = $("btnPrimary");
|
| 634 |
+
const story = $("story");
|
| 635 |
+
const qArea = $("qArea");
|
| 636 |
+
|
| 637 |
+
if (state.mode === "title") {
|
| 638 |
+
qArea.style.display = "none";
|
| 639 |
+
hideMsg();
|
| 640 |
+
story.textContent = [
|
| 641 |
+
"⚔️ Copilot Quest is a practice adventure built from your question bank.",
|
| 642 |
+
"",
|
| 643 |
+
"Goal: clear all regions without losing all HP.",
|
| 644 |
+
"",
|
| 645 |
+
"Press Start to begin."
|
| 646 |
+
].join("\n");
|
| 647 |
+
primary.textContent = "Start";
|
| 648 |
+
primary.onclick = () => {
|
| 649 |
+
playSfx('start');
|
| 650 |
+
tryPlayBgm();
|
| 651 |
+
state = structuredClone(DEFAULT);
|
| 652 |
+
state.startedAt = nowIso();
|
| 653 |
+
state.order = buildRunOrder();
|
| 654 |
+
state.mode = "intro";
|
| 655 |
+
state.regionId = 1;
|
| 656 |
+
state.pos = 0;
|
| 657 |
+
render();
|
| 658 |
+
};
|
| 659 |
+
return;
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
if (state.mode === "intro") {
|
| 663 |
+
qArea.style.display = "none";
|
| 664 |
+
hideMsg();
|
| 665 |
+
const region = REGIONS.find(r => r.id === state.regionId);
|
| 666 |
+
story.textContent = regionIntroText(region);
|
| 667 |
+
primary.textContent = "Enter Region";
|
| 668 |
+
primary.onclick = () => { playSfx('enter'); tryPlayBgm(); state.mode = "question"; render(); };
|
| 669 |
+
return;
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
if (state.mode === "question") {
|
| 673 |
+
hideMsg();
|
| 674 |
+
const q = getCurrentQuestion();
|
| 675 |
+
if (!q) { state.mode = "finished"; render(); return; }
|
| 676 |
+
|
| 677 |
+
if (q.topic !== state.regionId) { state.mode = "regionDone"; render(); return; }
|
| 678 |
+
|
| 679 |
+
story.textContent = "🗺️ " + flavorFor(q);
|
| 680 |
+
showQuestion(q);
|
| 681 |
+
primary.textContent = "Submit";
|
| 682 |
+
primary.onclick = () => {
|
| 683 |
+
playSfx('click');
|
| 684 |
+
const chosen = getSelected();
|
| 685 |
+
if (!chosen.length) { setMsg("warn", "Pick an answer first."); return; }
|
| 686 |
+
|
| 687 |
+
const correct = q.answer;
|
| 688 |
+
const ok = eqSet(chosen, correct);
|
| 689 |
+
state.last = { qid: q.id, chosen, correct, ok };
|
| 690 |
+
|
| 691 |
+
if (ok) {
|
| 692 |
+
playSfx('correct');
|
| 693 |
+
state.xp += q.multi ? 18 : 12;
|
| 694 |
+
state.gold += q.multi ? 9 : 6;
|
| 695 |
+
state.lvl = computeLevel(state.xp);
|
| 696 |
+
state.inventory.push({ ...relicFor(q), gainedAt: nowIso() });
|
| 697 |
+
playSfx('relic');
|
| 698 |
+
setMsg("good", `✅ Correct!\nYou gained XP and gold, and found a relic.\n\nCorrect answer: ${correct.join(", ")}`);
|
| 699 |
+
} else {
|
| 700 |
+
playSfx('wrong');
|
| 701 |
+
state.hp = clamp(state.hp - 1, 0, 3);
|
| 702 |
+
setMsg("bad", `❌ Not quite.\n\nYour answer: ${chosen.join(", ")}\nCorrect answer: ${correct.join(", ")}\n\nHP -1`);
|
| 703 |
+
}
|
| 704 |
+
state.mode = state.hp <= 0 ? "gameOver" : "feedback";
|
| 705 |
+
if (state.mode === "gameOver") playSfx('gameover');
|
| 706 |
+
render();
|
| 707 |
+
};
|
| 708 |
+
return;
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
if (state.mode === "feedback") {
|
| 712 |
+
primary.textContent = "Continue";
|
| 713 |
+
primary.onclick = () => {
|
| 714 |
+
playSfx('click');
|
| 715 |
+
clearSelected();
|
| 716 |
+
state.pos += 1;
|
| 717 |
+
const next = state.order[state.pos];
|
| 718 |
+
const nextQ = QUESTION_BANK.find(q => q.id === next);
|
| 719 |
+
if (!nextQ) state.mode = "finished";
|
| 720 |
+
if (!nextQ) playSfx('victory');
|
| 721 |
+
else if (nextQ.topic !== state.regionId) state.mode = "regionDone";
|
| 722 |
+
else state.mode = "question";
|
| 723 |
+
render();
|
| 724 |
+
};
|
| 725 |
+
Array.from(document.querySelectorAll("input[name='answer']")).forEach(i => i.disabled = true);
|
| 726 |
+
return;
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
if (state.mode === "regionDone") {
|
| 730 |
+
qArea.style.display = "none";
|
| 731 |
+
hideMsg();
|
| 732 |
+
const region = REGIONS.find(r => r.id === state.regionId);
|
| 733 |
+
const rp = regionProgress();
|
| 734 |
+
story.textContent = [
|
| 735 |
+
`🏁 Region cleared: ${region.title}`,
|
| 736 |
+
`You overcame ${rp.total} encounters here.`,
|
| 737 |
+
"",
|
| 738 |
+
`Reward: ${region.reward}`,
|
| 739 |
+
"",
|
| 740 |
+
"Press Continue to travel onward."
|
| 741 |
+
].join("\n");
|
| 742 |
+
primary.textContent = (state.regionId < REGIONS.length) ? "Continue" : "Finish";
|
| 743 |
+
primary.onclick = () => {
|
| 744 |
+
playSfx('region');
|
| 745 |
+
state.regionId = clamp(state.regionId + 1, 1, REGIONS.length);
|
| 746 |
+
state.mode = "intro";
|
| 747 |
+
render();
|
| 748 |
+
};
|
| 749 |
+
return;
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
if (state.mode === "finished") {
|
| 753 |
+
qArea.style.display = "none";
|
| 754 |
+
hideMsg();
|
| 755 |
+
const fp = fullProgress();
|
| 756 |
+
const score = Math.round((state.xp / (fp.total * 18)) * 100);
|
| 757 |
+
story.textContent = [
|
| 758 |
+
"🏆 Victory!",
|
| 759 |
+
"",
|
| 760 |
+
`You cleared the realm with Level ${state.lvl}.`,
|
| 761 |
+
`XP: ${state.xp} Gold: ${state.gold}`,
|
| 762 |
+
`Relics: ${state.inventory.length}`,
|
| 763 |
+
"",
|
| 764 |
+
`Completion: ${fp.done}/${fp.total}`,
|
| 765 |
+
`Final Score (rough): ${clamp(score, 0, 100)}%`,
|
| 766 |
+
"",
|
| 767 |
+
"Replay to shuffle encounters and reinforce memory."
|
| 768 |
+
].join("\n");
|
| 769 |
+
primary.textContent = "Play Again";
|
| 770 |
+
primary.onclick = () => { playSfx('click'); state.mode = "title"; render(); };
|
| 771 |
+
return;
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
if (state.mode === "gameOver") {
|
| 775 |
+
qArea.style.display = "none";
|
| 776 |
+
hideMsg();
|
| 777 |
+
story.textContent = [
|
| 778 |
+
"💀 Game Over.",
|
| 779 |
+
"",
|
| 780 |
+
"Your HP hit zero. The realm’s guardians demand more grounding and careful reading.",
|
| 781 |
+
"",
|
| 782 |
+
"Press Retry to start over."
|
| 783 |
+
].join("\n");
|
| 784 |
+
primary.textContent = "Retry";
|
| 785 |
+
primary.onclick = () => { playSfx('click'); state.mode = "title"; render(); };
|
| 786 |
+
return;
|
| 787 |
+
}
|
| 788 |
+
}
|
| 789 |
+
|
| 790 |
+
// ---------- persistence ----------
|
| 791 |
+
const KEY = "copilot-quest-save-v1";
|
| 792 |
+
function save() {
|
| 793 |
+
playSfx('save');
|
| 794 |
+
localStorage.setItem(KEY, JSON.stringify(state));
|
| 795 |
+
setMsg("good", "Saved to this browser.");
|
| 796 |
+
}
|
| 797 |
+
function load() {
|
| 798 |
+
playSfx('load');
|
| 799 |
+
const raw = localStorage.getItem(KEY);
|
| 800 |
+
if (!raw) { setMsg("warn", "No save found in this browser yet."); return; }
|
| 801 |
+
try {
|
| 802 |
+
const obj = JSON.parse(raw);
|
| 803 |
+
state = Object.assign(structuredClone(DEFAULT), obj);
|
| 804 |
+
setMsg("good", "Loaded!");
|
| 805 |
+
render();
|
| 806 |
+
} catch (e) {
|
| 807 |
+
setMsg("bad", "Could not load save (corrupted).");
|
| 808 |
+
}
|
| 809 |
+
}
|
| 810 |
+
function reset() {
|
| 811 |
+
playSfx('reset');
|
| 812 |
+
localStorage.removeItem(KEY);
|
| 813 |
+
state = structuredClone(DEFAULT);
|
| 814 |
+
render();
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
$("btnSave").onclick = save;
|
| 818 |
+
$("btnLoad").onclick = load;
|
| 819 |
+
$("btnReset").onclick = reset;
|
| 820 |
+
|
| 821 |
+
|
| 822 |
+
loadSfxPref();
|
| 823 |
+
setSoundButton();
|
| 824 |
+
const btnSound = $("btnSound");
|
| 825 |
+
|
| 826 |
+
loadBgmPref();
|
| 827 |
+
setMusicUi();
|
| 828 |
+
|
| 829 |
+
const btnMusic = $("btnMusic");
|
| 830 |
+
if (btnMusic) {
|
| 831 |
+
btnMusic.onclick = async () => {
|
| 832 |
+
bgmEnabled = !bgmEnabled;
|
| 833 |
+
saveBgmPref();
|
| 834 |
+
setMusicUi();
|
| 835 |
+
if (bgmEnabled) {
|
| 836 |
+
await tryPlayBgm();
|
| 837 |
+
playSfx("click");
|
| 838 |
+
} else {
|
| 839 |
+
pauseBgm();
|
| 840 |
+
playSfx("click");
|
| 841 |
+
}
|
| 842 |
+
};
|
| 843 |
+
}
|
| 844 |
+
|
| 845 |
+
const musicVol = $("musicVol");
|
| 846 |
+
if (musicVol) {
|
| 847 |
+
musicVol.oninput = () => {
|
| 848 |
+
bgmVolume = clamp(Number(musicVol.value) / 100, 0, 1);
|
| 849 |
+
if (bgm) bgm.volume = bgmVolume;
|
| 850 |
+
saveBgmPref();
|
| 851 |
+
};
|
| 852 |
+
}
|
| 853 |
+
|
| 854 |
+
// Pause music when tab is hidden; resume when visible (if enabled)
|
| 855 |
+
document.addEventListener("visibilitychange", () => {
|
| 856 |
+
if (document.hidden) {
|
| 857 |
+
pauseBgm();
|
| 858 |
+
} else {
|
| 859 |
+
tryPlayBgm();
|
| 860 |
+
}
|
| 861 |
+
});
|
| 862 |
+
|
| 863 |
+
|
| 864 |
+
if (btnSound) {
|
| 865 |
+
btnSound.onclick = () => {
|
| 866 |
+
sfxEnabled = !sfxEnabled;
|
| 867 |
+
saveSfxPref();
|
| 868 |
+
setSoundButton();
|
| 869 |
+
if (sfxEnabled) playSfx("click");
|
| 870 |
+
};
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
render();
|
| 874 |
+
</script>
|
| 875 |
+
</body>
|
| 876 |
+
</html>
|