termi-kids 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +34 -0
  2. package/README.md +148 -0
  3. package/SAFETY.md +187 -0
  4. package/bin/termi.js +22 -0
  5. package/dist/agent/context.js +126 -0
  6. package/dist/agent/loop.js +172 -0
  7. package/dist/agent/prompts/system.js +45 -0
  8. package/dist/agent/tools.js +335 -0
  9. package/dist/auth/keychain.js +146 -0
  10. package/dist/auth/oauth.js +375 -0
  11. package/dist/auth/tokens.js +219 -0
  12. package/dist/cli.js +258 -0
  13. package/dist/config/paths.js +92 -0
  14. package/dist/config/pin.js +150 -0
  15. package/dist/config/settings.js +131 -0
  16. package/dist/grownups/panel.js +483 -0
  17. package/dist/learn/lessons.js +490 -0
  18. package/dist/learn/runner.js +193 -0
  19. package/dist/preview/server.js +407 -0
  20. package/dist/projects/create.js +103 -0
  21. package/dist/projects/ideas.js +182 -0
  22. package/dist/projects/quests.js +277 -0
  23. package/dist/projects/scaffolds/art.js +484 -0
  24. package/dist/projects/scaffolds/biggames.js +554 -0
  25. package/dist/projects/scaffolds/characters.js +580 -0
  26. package/dist/projects/scaffolds/games.js +516 -0
  27. package/dist/projects/scaffolds/index.js +24 -0
  28. package/dist/projects/scaffolds/music.js +528 -0
  29. package/dist/projects/scaffolds/pets.js +567 -0
  30. package/dist/projects/scaffolds/quizzes.js +757 -0
  31. package/dist/projects/scaffolds/stories.js +620 -0
  32. package/dist/projects/scaffolds/vendor/KAPLAY-LICENSE.txt +35 -0
  33. package/dist/projects/scaffolds/vendor/kaplay.mjs +57 -0
  34. package/dist/projects/scaffolds/websites.js +474 -0
  35. package/dist/projects/snapshots.js +203 -0
  36. package/dist/projects/store.js +325 -0
  37. package/dist/providers/errors.js +207 -0
  38. package/dist/providers/index.js +316 -0
  39. package/dist/providers/models.js +38 -0
  40. package/dist/safety/audit.js +195 -0
  41. package/dist/safety/blocks.js +29 -0
  42. package/dist/safety/classifier.js +337 -0
  43. package/dist/safety/codescan.js +168 -0
  44. package/dist/safety/guarddownload.js +79 -0
  45. package/dist/safety/guardrunner.js +125 -0
  46. package/dist/safety/localguard.js +227 -0
  47. package/dist/safety/modelstore.js +127 -0
  48. package/dist/safety/prefilter.js +214 -0
  49. package/dist/safety/session.js +118 -0
  50. package/dist/safety/taxonomy.js +246 -0
  51. package/dist/safety/textextract.js +193 -0
  52. package/dist/setup/launcher.js +65 -0
  53. package/dist/setup/wizard.js +469 -0
  54. package/dist/surfaces/chat.js +439 -0
  55. package/dist/surfaces/commands.js +206 -0
  56. package/dist/surfaces/home.js +438 -0
  57. package/dist/types.js +5 -0
  58. package/dist/ui/banner.js +35 -0
  59. package/dist/ui/celebrate.js +141 -0
  60. package/dist/ui/errors.js +97 -0
  61. package/dist/ui/mascot.js +223 -0
  62. package/dist/ui/text.js +156 -0
  63. package/dist/ui/theme.js +92 -0
  64. package/package.json +67 -0
@@ -0,0 +1,620 @@
1
+ /**
2
+ * Story Quest: a choose-your-own-adventure scaffold.
3
+ * The kid edits the STORY object at the top of game.js.
4
+ * Engine: typewriter reveal, choice buttons, one item that unlocks
5
+ * one path, and endings tracked in localStorage.
6
+ */
7
+ function jsData(value) {
8
+ return JSON.stringify(value, null, 2);
9
+ }
10
+ function escapeHtml(text) {
11
+ return text
12
+ .replace(/&/g, '&')
13
+ .replace(/</g, '&lt;')
14
+ .replace(/>/g, '&gt;')
15
+ .replace(/"/g, '&quot;');
16
+ }
17
+ const dragonTheme = {
18
+ id: 'dragon-treasure',
19
+ label: 'Dragon Treasure',
20
+ emoji: '\u{1F432}',
21
+ palette: { bg: '#1c1033', fg: '#fff6e9', accent: '#ffb347' },
22
+ glyphs: { item: '\u{1F5DD}\u{FE0F}', lock: '\u{1F512}', ending: '\u{1F3C1}', spark: '\u{2728}' },
23
+ strings: {
24
+ tagline: 'A mountain of gold. A sleeping dragon. Your choices.',
25
+ theEnd: 'The End',
26
+ playAgain: 'Play again',
27
+ startOver: 'Start over',
28
+ foundAll: 'You found every ending! Amazing.',
29
+ moreLeft: 'More endings are hiding. Try a new path!',
30
+ pickupPrefix: 'You picked up the ',
31
+ tip: 'Tap the story text to show all the words. Edit STORY in game.js to change your tale.',
32
+ },
33
+ narrativeIntro: 'A dragon guards a mountain of gold. Your choices decide how the story ends.',
34
+ nonViolent: true,
35
+ nonCompetitive: true,
36
+ };
37
+ const mysteryTheme = {
38
+ id: 'mystery-school',
39
+ label: 'Mystery at School',
40
+ emoji: '\u{1F575}\u{FE0F}',
41
+ palette: { bg: '#102a43', fg: '#f0f4f8', accent: '#5ad1b3' },
42
+ glyphs: { item: '\u{1F511}', lock: '\u{1F512}', ending: '\u{1F3C6}', spark: '\u{1F50E}' },
43
+ strings: {
44
+ tagline: 'The trophy is gone. You are on the case, detective.',
45
+ theEnd: 'Case Closed',
46
+ playAgain: 'Play again',
47
+ startOver: 'Start over',
48
+ foundAll: 'You found every ending! Super sleuth.',
49
+ moreLeft: 'More endings are hiding. Follow a new clue!',
50
+ pickupPrefix: 'You picked up the ',
51
+ tip: 'Tap the story text to show all the words. Edit STORY in game.js to change the case.',
52
+ },
53
+ narrativeIntro: 'The school trophy vanished overnight. Your choices crack the case.',
54
+ nonViolent: true,
55
+ nonCompetitive: true,
56
+ };
57
+ const dragonStory = {
58
+ start: 'gates',
59
+ scenes: [
60
+ {
61
+ id: 'gates',
62
+ text: 'Ember Mountain rises ahead. Smoke curls from the top. A stone gate stands open, and a cold stream slips past the rocks.',
63
+ choices: [
64
+ { label: 'Step through the dark gate', goto: 'cave' },
65
+ { label: 'Follow the stream', goto: 'stream' },
66
+ ],
67
+ },
68
+ {
69
+ id: 'cave',
70
+ text: 'Inside, glowing moss lights the walls. Two tunnels split ahead. The left one sparkles. The right one smells like old metal.',
71
+ choices: [
72
+ { label: 'Take the sparkling left tunnel', goto: 'moss' },
73
+ { label: 'Take the metal smelling right tunnel', goto: 'mine' },
74
+ ],
75
+ },
76
+ {
77
+ id: 'stream',
78
+ text: "A huge turtle floats by with a map painted on its shell. It blinks slowly. 'Looking for the dragon treasure?' it asks.",
79
+ choices: [
80
+ { label: 'Ask the turtle for help', goto: 'turtle' },
81
+ { label: 'Wade across the cold water', goto: 'waterfall' },
82
+ ],
83
+ },
84
+ {
85
+ id: 'turtle',
86
+ text: "'The vault has a silver lock,' says the turtle. 'Miners hid the key below. Look where the carts sleep.'",
87
+ choices: [
88
+ { label: 'Search the old mine', goto: 'mine' },
89
+ { label: 'Head through the mountain gate', goto: 'cave' },
90
+ ],
91
+ },
92
+ {
93
+ id: 'mine',
94
+ text: 'Dusty mine carts rest on bent rails. Something shines under a wheel. It is a small silver key!',
95
+ gives: 'silver key',
96
+ choices: [
97
+ { label: 'Ride a cart deeper inside', goto: 'hall' },
98
+ { label: 'Squeeze through a glowing crack', goto: 'moss' },
99
+ ],
100
+ },
101
+ {
102
+ id: 'moss',
103
+ text: 'This room hums softly. Sleepy bats hang in rows, like socks on a line.',
104
+ choices: [
105
+ { label: 'Tiptoe past the bats', goto: 'hall' },
106
+ { label: 'Hum your favorite song', goto: 'dragon' },
107
+ ],
108
+ },
109
+ {
110
+ id: 'hall',
111
+ text: 'You reach a giant hall. A snoring dragon sleeps on a hill of gold. Behind it stands a silver door with a tiny keyhole.',
112
+ choices: [
113
+ { label: 'Open the silver door', goto: 'vault', needs: 'silver key' },
114
+ { label: 'Wake the dragon politely', goto: 'dragon' },
115
+ { label: 'Sneak back to the gate', goto: 'gates' },
116
+ ],
117
+ },
118
+ {
119
+ id: 'dragon',
120
+ text: "The dragon lifts its head and looks right at you. 'Few visitors are this polite,' it rumbles. 'I have been alone a long time.'",
121
+ choices: [
122
+ { label: 'Share your sandwich', goto: 'friend' },
123
+ { label: 'Ask for a riddle', goto: 'riddle' },
124
+ ],
125
+ },
126
+ {
127
+ id: 'riddle',
128
+ text: "'Answer this and I will like you even more,' says the dragon. 'What has to be broken before you can use it?'",
129
+ choices: [
130
+ { label: 'Say: an egg', goto: 'friend' },
131
+ { label: 'Say: a secret', goto: 'hall' },
132
+ ],
133
+ },
134
+ {
135
+ id: 'waterfall',
136
+ text: 'The stream speeds up. Whoosh! You slide down a hidden waterfall into a sunny lake. Village kids cheer your giant splash. You did not find gold today. You found the best shortcut in the valley.',
137
+ choices: [],
138
+ },
139
+ {
140
+ id: 'vault',
141
+ text: 'The silver key turns with a soft click. Inside, gold coins glow like little suns. You fill one bag and leave the rest for the dragon. Back home, the whole village celebrates you.',
142
+ choices: [],
143
+ },
144
+ {
145
+ id: 'friend',
146
+ text: "The dragon grins, warm as a campfire. 'Stay for tea,' it says. You talk for hours about maps, gold, and bats. You made a legendary friend. You visit every Saturday.",
147
+ choices: [],
148
+ },
149
+ ],
150
+ };
151
+ const mysteryStory = {
152
+ start: 'hallway',
153
+ scenes: [
154
+ {
155
+ id: 'hallway',
156
+ text: "The glass trophy case is empty! Coach's big gold cup is gone. Muddy footprints lead two ways down the hall.",
157
+ choices: [
158
+ { label: 'Follow the prints to the gym', goto: 'gym' },
159
+ { label: 'Check the library for clues', goto: 'library' },
160
+ ],
161
+ },
162
+ {
163
+ id: 'gym',
164
+ text: 'The gym smells like floor wax. The prints stop near the supply closet. Coach Reyes is stacking cones nearby.',
165
+ choices: [
166
+ { label: 'Ask Coach Reyes', goto: 'coach' },
167
+ { label: 'Peek down the basement stairs', goto: 'basement' },
168
+ ],
169
+ },
170
+ {
171
+ id: 'coach',
172
+ text: "'The trophy? Janitor Lee carried a big box this morning,' says Coach. 'He went toward the music hall.'",
173
+ choices: [
174
+ { label: 'Find Janitor Lee', goto: 'janitor' },
175
+ { label: 'Head to the basement stairs', goto: 'basement' },
176
+ ],
177
+ },
178
+ {
179
+ id: 'library',
180
+ text: "The library is quiet as snow. Ms. Patel whispers, 'Strange things end up in lost and found.' Old yearbooks sit on a cart.",
181
+ choices: [
182
+ { label: 'Dig through lost and found', goto: 'lostfound' },
183
+ { label: 'Open the dusty yearbooks', goto: 'yearbook' },
184
+ ],
185
+ },
186
+ {
187
+ id: 'lostfound',
188
+ text: 'You find one mitten, three whistles, and a small brass key. The tag says BASEMENT.',
189
+ gives: 'brass key',
190
+ choices: [
191
+ { label: 'Take the stairs to the basement', goto: 'basement' },
192
+ { label: 'Check the gym next', goto: 'gym' },
193
+ ],
194
+ },
195
+ {
196
+ id: 'yearbook',
197
+ text: 'A photo from 1989 shows a hidden room behind the music hall curtain. Students called it the Polish Club.',
198
+ choices: [
199
+ { label: 'Go to the music hall', goto: 'music' },
200
+ { label: 'Try the basement instead', goto: 'basement' },
201
+ ],
202
+ },
203
+ {
204
+ id: 'basement',
205
+ text: 'At the bottom of the stairs waits a green door. It is locked tight. You hear soft laughing inside.',
206
+ choices: [
207
+ { label: 'Unlock the green door', goto: 'clubroom', needs: 'brass key' },
208
+ { label: 'Knock three times', goto: 'knock' },
209
+ { label: 'Go back upstairs', goto: 'hallway' },
210
+ ],
211
+ },
212
+ {
213
+ id: 'knock',
214
+ text: "A voice whispers, 'Shh! It is not Friday yet!' Then quiet feet shuffle away inside.",
215
+ choices: [
216
+ { label: 'Press your ear to the door', goto: 'listen' },
217
+ { label: 'Go ask Janitor Lee', goto: 'janitor' },
218
+ ],
219
+ },
220
+ {
221
+ id: 'janitor',
222
+ text: "Janitor Lee smiles. 'My box? Just ribbons for Friday,' he says. 'Funny week. Kids keep sneaking toward the music hall.'",
223
+ choices: [
224
+ { label: 'Search the music hall', goto: 'music' },
225
+ { label: 'Return to the basement door', goto: 'basement' },
226
+ ],
227
+ },
228
+ {
229
+ id: 'music',
230
+ text: 'Behind the heavy curtain, you find them. The student council is polishing the trophy until it shines! It is a surprise for Coach on Friday. You promise to keep the secret. They hand you a polishing cloth. Welcome to the team, detective.',
231
+ choices: [],
232
+ },
233
+ {
234
+ id: 'clubroom',
235
+ text: 'The brass key clicks. Inside the old Polish Club room, the trophy sparkles on a velvet pillow. A banner reads SURPRISE PARTY FOR COACH. You help plan the big reveal. Friday is going to be great.',
236
+ choices: [],
237
+ },
238
+ {
239
+ id: 'listen',
240
+ text: 'You hear the whole plan. A surprise assembly! You tiptoe upstairs and zip your lips. On Friday, you act amazed with everyone else. Best secret ever kept.',
241
+ choices: [],
242
+ },
243
+ ],
244
+ };
245
+ function buildHtml(theme, prettyName) {
246
+ const s = theme.strings;
247
+ return `<!DOCTYPE html>
248
+ <html lang="en">
249
+ <head>
250
+ <meta charset="UTF-8">
251
+ <meta name="viewport" content="width=device-width, initial-scale=1">
252
+ <title>${escapeHtml(prettyName)}</title>
253
+ <link rel="icon" href="data:,">
254
+ <link rel="stylesheet" href="style.css">
255
+ <script src="game.js" defer></script>
256
+ </head>
257
+ <body>
258
+ <main class="frame">
259
+ <header>
260
+ <h1><span class="emoji">${theme.emoji}</span> ${escapeHtml(prettyName)}</h1>
261
+ <p class="tagline">${s['tagline'] ?? ''}</p>
262
+ </header>
263
+ <section class="card" id="story-card">
264
+ <p id="scene-text"></p>
265
+ <div id="toast" class="toast hidden"></div>
266
+ <div id="choices" class="choices"></div>
267
+ </section>
268
+ <footer>
269
+ <span id="endings-note"></span>
270
+ <button id="restart" class="ghost" type="button">${s['startOver'] ?? 'Start over'}</button>
271
+ </footer>
272
+ <p class="tip">${s['tip'] ?? ''}</p>
273
+ </main>
274
+ </body>
275
+ </html>
276
+ `;
277
+ }
278
+ function buildCss(theme) {
279
+ const p = theme.palette;
280
+ return `/* Story Quest styles. Colors come from your THEME in game.js. */
281
+ * { box-sizing: border-box; }
282
+ :root {
283
+ --bg: ${p.bg};
284
+ --fg: ${p.fg};
285
+ --accent: ${p.accent};
286
+ }
287
+ body {
288
+ margin: 0;
289
+ min-height: 100vh;
290
+ background: var(--bg);
291
+ background-image: radial-gradient(circle at 18% 0%, color-mix(in srgb, var(--accent) 16%, var(--bg)), var(--bg) 60%);
292
+ color: var(--fg);
293
+ font-family: system-ui, "Segoe UI", Roboto, Arial, sans-serif;
294
+ display: flex;
295
+ justify-content: center;
296
+ padding: 28px 16px;
297
+ }
298
+ .frame { width: min(680px, 100%); }
299
+ header h1 { font-size: 1.7rem; margin: 0 0 4px; }
300
+ header .emoji { font-size: 1.6rem; }
301
+ .tagline { margin: 0 0 18px; opacity: 0.85; }
302
+ .card {
303
+ background: rgba(255, 255, 255, 0.07);
304
+ background: color-mix(in srgb, var(--fg) 8%, var(--bg));
305
+ border: 1px solid color-mix(in srgb, var(--fg) 16%, var(--bg));
306
+ border-radius: 16px;
307
+ padding: 22px;
308
+ cursor: pointer;
309
+ }
310
+ #scene-text {
311
+ font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
312
+ font-size: 1.15rem;
313
+ line-height: 1.65;
314
+ min-height: 6.5em;
315
+ margin: 0 0 16px;
316
+ white-space: pre-wrap;
317
+ }
318
+ .toast {
319
+ background: var(--accent);
320
+ color: var(--bg);
321
+ font-weight: 700;
322
+ border-radius: 10px;
323
+ padding: 8px 12px;
324
+ margin: 0 0 14px;
325
+ animation: pop 0.25s ease-out;
326
+ }
327
+ .hidden { display: none; }
328
+ .choices { display: grid; gap: 10px; }
329
+ .choice {
330
+ background: var(--accent);
331
+ color: var(--bg);
332
+ border: none;
333
+ border-radius: 12px;
334
+ padding: 12px 16px;
335
+ font-size: 1rem;
336
+ font-weight: 700;
337
+ text-align: left;
338
+ cursor: pointer;
339
+ transition: transform 0.12s ease;
340
+ }
341
+ .choice:hover:not(:disabled) { transform: translateY(-2px); }
342
+ .choice:focus-visible { outline: 3px solid var(--fg); }
343
+ .choice:disabled { opacity: 0.45; cursor: not-allowed; }
344
+ .the-end {
345
+ font-family: Georgia, serif;
346
+ font-style: italic;
347
+ font-size: 1.5rem;
348
+ text-align: center;
349
+ margin: 4px 0;
350
+ }
351
+ .endings-line { text-align: center; margin: 4px 0 10px; opacity: 0.9; }
352
+ footer {
353
+ display: flex;
354
+ justify-content: space-between;
355
+ align-items: center;
356
+ margin-top: 14px;
357
+ }
358
+ .ghost {
359
+ background: transparent;
360
+ color: var(--fg);
361
+ border: 1px solid color-mix(in srgb, var(--fg) 40%, var(--bg));
362
+ border-radius: 10px;
363
+ padding: 8px 14px;
364
+ cursor: pointer;
365
+ }
366
+ .ghost:hover { border-color: var(--accent); color: var(--accent); }
367
+ .tip { opacity: 0.65; font-size: 0.85rem; margin-top: 16px; }
368
+ @keyframes pop { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
369
+ `;
370
+ }
371
+ function buildGameJs(theme, story) {
372
+ return `// Story Quest: ${theme.label}. Made with Termi.
373
+ //
374
+ // HOW YOUR STORY WORKS:
375
+ // 1. Every scene lives in the STORY object below. Change any "text"!
376
+ // 2. A choice's "goto" must match another scene's "id".
377
+ // 3. "gives" hands the player an item. "needs" locks a choice.
378
+ // 4. A scene with an empty "choices" list is an ending.
379
+ // Save the file and refresh the page to see your changes.
380
+
381
+ const THEME = ${jsData({
382
+ id: theme.id,
383
+ label: theme.label,
384
+ emoji: theme.emoji,
385
+ palette: theme.palette,
386
+ glyphs: theme.glyphs,
387
+ strings: theme.strings,
388
+ })};
389
+
390
+ // === YOUR STORY (edit this part!) ===
391
+ const STORY = ${jsData(story)};
392
+ // === END OF YOUR STORY ===
393
+
394
+ // ----- The story engine starts here. Curious? Read on! -----
395
+ const sceneTextEl = document.getElementById("scene-text");
396
+ const choicesEl = document.getElementById("choices");
397
+ const toastEl = document.getElementById("toast");
398
+ const endingsEl = document.getElementById("endings-note");
399
+ const restartBtn = document.getElementById("restart");
400
+ const storyCard = document.getElementById("story-card");
401
+
402
+ const ENDINGS_KEY = "termi-story-endings-" + THEME.id;
403
+ let items = [];
404
+ let typingTimer = null;
405
+ let finishTyping = null;
406
+ let toastTimer = null;
407
+
408
+ function loadEndings() {
409
+ try {
410
+ const raw = localStorage.getItem(ENDINGS_KEY);
411
+ return raw ? JSON.parse(raw) : [];
412
+ } catch (err) {
413
+ return [];
414
+ }
415
+ }
416
+
417
+ function saveEndings(list) {
418
+ try {
419
+ localStorage.setItem(ENDINGS_KEY, JSON.stringify(list));
420
+ } catch (err) {
421
+ // Storage is off. The game still plays fine.
422
+ }
423
+ }
424
+
425
+ function allEndings() {
426
+ return STORY.scenes.filter(function (scene) {
427
+ return scene.choices.length === 0;
428
+ });
429
+ }
430
+
431
+ function findScene(id) {
432
+ return STORY.scenes.find(function (scene) {
433
+ return scene.id === id;
434
+ });
435
+ }
436
+
437
+ function hasItem(name) {
438
+ return items.indexOf(name) !== -1;
439
+ }
440
+
441
+ function showToast(message) {
442
+ toastEl.textContent = message;
443
+ toastEl.classList.remove("hidden");
444
+ clearTimeout(toastTimer);
445
+ toastTimer = setTimeout(function () {
446
+ toastEl.classList.add("hidden");
447
+ }, 2600);
448
+ }
449
+
450
+ function updateEndingsNote() {
451
+ const found = loadEndings().length;
452
+ const total = allEndings().length;
453
+ endingsEl.textContent = THEME.glyphs.ending + " You found " + found + " of " + total + " endings!";
454
+ }
455
+
456
+ function typeText(text, done) {
457
+ clearInterval(typingTimer);
458
+ sceneTextEl.textContent = "";
459
+ let shown = 0;
460
+ finishTyping = function () {
461
+ clearInterval(typingTimer);
462
+ typingTimer = null;
463
+ finishTyping = null;
464
+ sceneTextEl.textContent = text;
465
+ done();
466
+ };
467
+ typingTimer = setInterval(function () {
468
+ shown += 1;
469
+ sceneTextEl.textContent = text.slice(0, shown);
470
+ if (shown >= text.length && finishTyping) {
471
+ finishTyping();
472
+ }
473
+ }, 18);
474
+ }
475
+
476
+ function makeChoiceButton(choice) {
477
+ const btn = document.createElement("button");
478
+ btn.className = "choice";
479
+ btn.type = "button";
480
+ if (choice.needs && !hasItem(choice.needs)) {
481
+ btn.textContent = THEME.glyphs.lock + " " + choice.label + " (needs the " + choice.needs + ")";
482
+ btn.disabled = true;
483
+ } else {
484
+ let label = choice.label;
485
+ if (choice.needs) {
486
+ label = THEME.glyphs.item + " " + label;
487
+ }
488
+ btn.textContent = label;
489
+ btn.addEventListener("click", function (event) {
490
+ event.stopPropagation();
491
+ showScene(choice.goto);
492
+ });
493
+ }
494
+ return btn;
495
+ }
496
+
497
+ function showEnding(scene) {
498
+ const found = loadEndings();
499
+ if (found.indexOf(scene.id) === -1) {
500
+ found.push(scene.id);
501
+ saveEndings(found);
502
+ }
503
+ updateEndingsNote();
504
+ const banner = document.createElement("p");
505
+ banner.className = "the-end";
506
+ banner.textContent = THEME.glyphs.ending + " " + THEME.strings.theEnd;
507
+ choicesEl.appendChild(banner);
508
+ const note = document.createElement("p");
509
+ note.className = "endings-line";
510
+ const total = allEndings().length;
511
+ note.textContent = found.length >= total ? THEME.strings.foundAll : THEME.strings.moreLeft;
512
+ choicesEl.appendChild(note);
513
+ const again = document.createElement("button");
514
+ again.className = "choice";
515
+ again.type = "button";
516
+ again.textContent = THEME.glyphs.spark + " " + THEME.strings.playAgain;
517
+ again.addEventListener("click", function (event) {
518
+ event.stopPropagation();
519
+ restart();
520
+ });
521
+ choicesEl.appendChild(again);
522
+ }
523
+
524
+ function showScene(id) {
525
+ const scene = findScene(id);
526
+ choicesEl.textContent = "";
527
+ if (!scene) {
528
+ sceneTextEl.textContent = 'Hmm, the scene "' + id + '" is missing. Check the goto spelling in game.js.';
529
+ return;
530
+ }
531
+ if (scene.gives && !hasItem(scene.gives)) {
532
+ items.push(scene.gives);
533
+ showToast(THEME.glyphs.item + " " + THEME.strings.pickupPrefix + scene.gives + "!");
534
+ }
535
+ typeText(scene.text, function () {
536
+ if (scene.choices.length === 0) {
537
+ showEnding(scene);
538
+ return;
539
+ }
540
+ scene.choices.forEach(function (choice) {
541
+ choicesEl.appendChild(makeChoiceButton(choice));
542
+ });
543
+ });
544
+ }
545
+
546
+ function restart() {
547
+ items = [];
548
+ showScene(STORY.start);
549
+ }
550
+
551
+ storyCard.addEventListener("click", function () {
552
+ if (finishTyping) {
553
+ finishTyping();
554
+ }
555
+ });
556
+ restartBtn.addEventListener("click", restart);
557
+
558
+ updateEndingsNote();
559
+ restart();
560
+ `;
561
+ }
562
+ function buildTermiMd(theme, prettyName, story) {
563
+ const endings = story.scenes.filter((s) => s.choices.length === 0).length;
564
+ return [
565
+ `# ${prettyName}`,
566
+ '',
567
+ '## What this is',
568
+ `A choose your own adventure game. The ${theme.label} story has ${story.scenes.length} scenes and ${endings} endings.`,
569
+ '',
570
+ '## Files',
571
+ '- index.html: the page and its parts.',
572
+ '- style.css: the colors and look.',
573
+ '- game.js: the STORY scenes at the top, then the story engine.',
574
+ '',
575
+ '## Built so far',
576
+ `- Starter ${theme.label} adventure with ${endings} endings and one hidden item.`,
577
+ '',
578
+ '## Recap line',
579
+ `We started a ${theme.label} adventure with ${endings} endings to find.`,
580
+ '',
581
+ ].join('\n');
582
+ }
583
+ function storyFor(theme) {
584
+ return theme.id === 'mystery-school' ? mysteryStory : dragonStory;
585
+ }
586
+ export const storiesScaffold = {
587
+ id: 'stories',
588
+ label: 'Story Quest',
589
+ emoji: '\u{1F4D6}',
590
+ ageNote: 'Great for ages 9 and up. You write the scenes, the code runs the adventure.',
591
+ themes: [dragonTheme, mysteryTheme],
592
+ files(theme, prettyName) {
593
+ const story = storyFor(theme);
594
+ return {
595
+ 'index.html': buildHtml(theme, prettyName),
596
+ 'style.css': buildCss(theme),
597
+ 'game.js': buildGameJs(theme, story),
598
+ 'TERMI.md': buildTermiMd(theme, prettyName, story),
599
+ };
600
+ },
601
+ starterPrompts(theme) {
602
+ if (theme.id === 'mystery-school') {
603
+ return [
604
+ 'Add a scene where I question the art teacher',
605
+ 'Make a new ending where the principal throws a pizza party',
606
+ 'Add a sneaky clue in the gym that tricks me',
607
+ 'Let me find a flashlight that unlocks a dark closet',
608
+ 'Make the story words show up faster',
609
+ ];
610
+ }
611
+ return [
612
+ 'Add a scene where the dragon shows me its egg collection',
613
+ 'Make a fourth ending where I become the mountain guard',
614
+ 'Add a funny talking squirrel to the stream scene',
615
+ 'Make the cave scene sound spookier',
616
+ 'Add a magic lantern item that unlocks a new path',
617
+ ];
618
+ },
619
+ };
620
+ export default storiesScaffold;
@@ -0,0 +1,35 @@
1
+ Copyright (c) 2025, KAPLAY Team and contributers
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions.
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ In addition, the following restrictions apply:
14
+
15
+ 1. The Software and any modifications made to it may not be used for the purpose
16
+ of training or improving machine learning algorithms, including but not
17
+ limited to artificial intelligence, natural language processing, or data
18
+ mining. This condition applies to any derivatives, modifications, or updates
19
+ based on the Software code. Any usage of the Software in an AI-training
20
+ dataset is considered a breach of this License.
21
+
22
+ 2. The Software may not be included in any dataset used for training or
23
+ improving machine learning algorithms, including but not limited to
24
+ artificial intelligence, natural language processing, or data mining.
25
+
26
+ 3. Any person or organization found to be in violation of these restrictions
27
+ will be subject to legal action and may be held liable for any damages
28
+ resulting from such use.
29
+
30
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
32
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
33
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
34
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
35
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.