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.
- package/LICENSE +34 -0
- package/README.md +148 -0
- package/SAFETY.md +187 -0
- package/bin/termi.js +22 -0
- package/dist/agent/context.js +126 -0
- package/dist/agent/loop.js +172 -0
- package/dist/agent/prompts/system.js +45 -0
- package/dist/agent/tools.js +335 -0
- package/dist/auth/keychain.js +146 -0
- package/dist/auth/oauth.js +375 -0
- package/dist/auth/tokens.js +219 -0
- package/dist/cli.js +258 -0
- package/dist/config/paths.js +92 -0
- package/dist/config/pin.js +150 -0
- package/dist/config/settings.js +131 -0
- package/dist/grownups/panel.js +483 -0
- package/dist/learn/lessons.js +490 -0
- package/dist/learn/runner.js +193 -0
- package/dist/preview/server.js +407 -0
- package/dist/projects/create.js +103 -0
- package/dist/projects/ideas.js +182 -0
- package/dist/projects/quests.js +277 -0
- package/dist/projects/scaffolds/art.js +484 -0
- package/dist/projects/scaffolds/biggames.js +554 -0
- package/dist/projects/scaffolds/characters.js +580 -0
- package/dist/projects/scaffolds/games.js +516 -0
- package/dist/projects/scaffolds/index.js +24 -0
- package/dist/projects/scaffolds/music.js +528 -0
- package/dist/projects/scaffolds/pets.js +567 -0
- package/dist/projects/scaffolds/quizzes.js +757 -0
- package/dist/projects/scaffolds/stories.js +620 -0
- package/dist/projects/scaffolds/vendor/KAPLAY-LICENSE.txt +35 -0
- package/dist/projects/scaffolds/vendor/kaplay.mjs +57 -0
- package/dist/projects/scaffolds/websites.js +474 -0
- package/dist/projects/snapshots.js +203 -0
- package/dist/projects/store.js +325 -0
- package/dist/providers/errors.js +207 -0
- package/dist/providers/index.js +316 -0
- package/dist/providers/models.js +38 -0
- package/dist/safety/audit.js +195 -0
- package/dist/safety/blocks.js +29 -0
- package/dist/safety/classifier.js +337 -0
- package/dist/safety/codescan.js +168 -0
- package/dist/safety/guarddownload.js +79 -0
- package/dist/safety/guardrunner.js +125 -0
- package/dist/safety/localguard.js +227 -0
- package/dist/safety/modelstore.js +127 -0
- package/dist/safety/prefilter.js +214 -0
- package/dist/safety/session.js +118 -0
- package/dist/safety/taxonomy.js +246 -0
- package/dist/safety/textextract.js +193 -0
- package/dist/setup/launcher.js +65 -0
- package/dist/setup/wizard.js +469 -0
- package/dist/surfaces/chat.js +439 -0
- package/dist/surfaces/commands.js +206 -0
- package/dist/surfaces/home.js +438 -0
- package/dist/types.js +5 -0
- package/dist/ui/banner.js +35 -0
- package/dist/ui/celebrate.js +141 -0
- package/dist/ui/errors.js +97 -0
- package/dist/ui/mascot.js +223 -0
- package/dist/ui/text.js +156 -0
- package/dist/ui/theme.js +92 -0
- package/package.json +67 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Sky Dash" scaffold: a canvas dodge game.
|
|
3
|
+
* One template, four themes. Theme data is interpolated into game.js
|
|
4
|
+
* as a const THEME block so kids and the AI can tweak it easily.
|
|
5
|
+
*/
|
|
6
|
+
const themes = [
|
|
7
|
+
{
|
|
8
|
+
id: 'space-rocks',
|
|
9
|
+
label: 'Space Rocks',
|
|
10
|
+
emoji: '🚀',
|
|
11
|
+
palette: { bg: '#0b1026', fg: '#e8ecff', accent: '#7dd3fc' },
|
|
12
|
+
glyphs: { player: '🚀', obstacle: '🪨', bonus: '⭐' },
|
|
13
|
+
strings: {
|
|
14
|
+
obstacleWord: 'space rocks',
|
|
15
|
+
bonusWord: 'stars',
|
|
16
|
+
scoreLabel: 'Score',
|
|
17
|
+
startHint: 'Press any key or tap to launch.',
|
|
18
|
+
controlsHint: 'Move with arrow keys, WASD, or your finger.',
|
|
19
|
+
retryHint: 'Press R, Space, or tap to fly again.',
|
|
20
|
+
winTitle: 'You Made It!',
|
|
21
|
+
winLine: 'You flew through the whole rock belt. The space station cheers for you.',
|
|
22
|
+
loseTitle: 'Bonk!',
|
|
23
|
+
loseLine: 'A space rock bumped your ship. The ship is fine. Ready to fly again?',
|
|
24
|
+
},
|
|
25
|
+
narrativeIntro: 'You are a brave pilot in the rock belt. Dodge the space rocks. Grab the stars. Reach 150 points to get home.',
|
|
26
|
+
nonViolent: true,
|
|
27
|
+
nonCompetitive: true,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'neon-star-run',
|
|
31
|
+
label: 'Neon Star Run',
|
|
32
|
+
emoji: '🌟',
|
|
33
|
+
palette: { bg: '#10001f', fg: '#f5e9ff', accent: '#ff4fd8' },
|
|
34
|
+
glyphs: { player: '🏃', obstacle: '🔻', bonus: '🌟' },
|
|
35
|
+
strings: {
|
|
36
|
+
obstacleWord: 'laser spikes',
|
|
37
|
+
bonusWord: 'neon stars',
|
|
38
|
+
scoreLabel: 'Score',
|
|
39
|
+
startHint: 'Press any key or tap to start the run.',
|
|
40
|
+
retryHint: 'Press R, Space, or tap to run again.',
|
|
41
|
+
controlsHint: 'Move with arrow keys, WASD, or your finger.',
|
|
42
|
+
winTitle: 'Track Champion!',
|
|
43
|
+
winLine: 'You lit up the whole track. The neon city glows just for you.',
|
|
44
|
+
loseTitle: 'Zapped!',
|
|
45
|
+
loseLine: 'A laser spike caught you. Shake it off, star runner. Go again?',
|
|
46
|
+
},
|
|
47
|
+
narrativeIntro: 'Night race in the neon city. Dodge the laser spikes. Grab neon stars. Hit 150 points to set the track record.',
|
|
48
|
+
nonViolent: true,
|
|
49
|
+
nonCompetitive: false,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'spooky-bats',
|
|
53
|
+
label: 'Spooky Bats',
|
|
54
|
+
emoji: '🦇',
|
|
55
|
+
palette: { bg: '#161021', fg: '#efe6d8', accent: '#ff9b3d' },
|
|
56
|
+
glyphs: { player: '🧙', obstacle: '🦇', bonus: '🍬' },
|
|
57
|
+
strings: {
|
|
58
|
+
obstacleWord: 'bats',
|
|
59
|
+
bonusWord: 'candy',
|
|
60
|
+
scoreLabel: 'Score',
|
|
61
|
+
startHint: 'Press any key or tap to take off.',
|
|
62
|
+
controlsHint: 'Move with arrow keys, WASD, or your finger.',
|
|
63
|
+
retryHint: 'Press R, Space, or tap to fly again.',
|
|
64
|
+
winTitle: 'Sky Crossed!',
|
|
65
|
+
winLine: 'You crossed the haunted sky. The bats squeak a friendly goodnight.',
|
|
66
|
+
loseTitle: 'Swooped!',
|
|
67
|
+
loseLine: 'A bat swooped you! It only wanted a hug. Try the night sky again?',
|
|
68
|
+
},
|
|
69
|
+
narrativeIntro: 'It is a spooky night flight. Dodge the swooping bats. Catch falling candy. Reach 150 points to cross the sky.',
|
|
70
|
+
nonViolent: false,
|
|
71
|
+
nonCompetitive: true,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'soccer-headers',
|
|
75
|
+
label: 'Soccer Headers',
|
|
76
|
+
emoji: '⚽',
|
|
77
|
+
palette: { bg: '#0e3320', fg: '#f2fff7', accent: '#ffd24a' },
|
|
78
|
+
glyphs: { player: '🧤', obstacle: '🟥', bonus: '⚽' },
|
|
79
|
+
strings: {
|
|
80
|
+
obstacleWord: 'red cards',
|
|
81
|
+
bonusWord: 'soccer balls',
|
|
82
|
+
scoreLabel: 'Score',
|
|
83
|
+
startHint: 'Press any key or tap for kickoff.',
|
|
84
|
+
controlsHint: 'Move with arrow keys, WASD, or your finger.',
|
|
85
|
+
retryHint: 'Press R, Space, or tap for a rematch.',
|
|
86
|
+
winTitle: 'Champions!',
|
|
87
|
+
winLine: 'What a save streak! The crowd goes wild. You win the cup.',
|
|
88
|
+
loseTitle: 'Red Card!',
|
|
89
|
+
loseLine: 'Oof, a red card hit you. Even pros miss sometimes. Rematch?',
|
|
90
|
+
},
|
|
91
|
+
narrativeIntro: 'Big match day. Dodge the red cards. Catch soccer balls for extra points. Reach 150 to win the cup.',
|
|
92
|
+
nonViolent: true,
|
|
93
|
+
nonCompetitive: false,
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
/** Builds the JSON for the const THEME block at the top of game.js. */
|
|
97
|
+
function themeBlock(theme) {
|
|
98
|
+
return JSON.stringify({
|
|
99
|
+
id: theme.id,
|
|
100
|
+
label: theme.label,
|
|
101
|
+
palette: theme.palette,
|
|
102
|
+
glyphs: theme.glyphs,
|
|
103
|
+
strings: theme.strings,
|
|
104
|
+
narrative: theme.narrativeIntro,
|
|
105
|
+
}, null, 2);
|
|
106
|
+
}
|
|
107
|
+
/** Makes a name safe to drop into HTML text. */
|
|
108
|
+
function escapeHtml(text) {
|
|
109
|
+
return text
|
|
110
|
+
.replace(/&/g, '&')
|
|
111
|
+
.replace(/</g, '<')
|
|
112
|
+
.replace(/>/g, '>')
|
|
113
|
+
.replace(/"/g, '"')
|
|
114
|
+
.replace(/'/g, ''');
|
|
115
|
+
}
|
|
116
|
+
/** Keeps a kid-typed name to one tidy line. */
|
|
117
|
+
function cleanName(name) {
|
|
118
|
+
const tidy = name.replace(/[\r\n\t]+/g, ' ').replace(/ {2,}/g, ' ').trim();
|
|
119
|
+
return tidy.length > 0 ? tidy : 'My Game';
|
|
120
|
+
}
|
|
121
|
+
function str(theme, key) {
|
|
122
|
+
return theme.strings[key] ?? '';
|
|
123
|
+
}
|
|
124
|
+
function indexHtml(theme, prettyName) {
|
|
125
|
+
const name = escapeHtml(cleanName(prettyName));
|
|
126
|
+
return `<!DOCTYPE html>
|
|
127
|
+
<html lang="en">
|
|
128
|
+
<head>
|
|
129
|
+
<meta charset="UTF-8" />
|
|
130
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
131
|
+
<title>${name}</title>
|
|
132
|
+
<link rel="stylesheet" href="style.css" />
|
|
133
|
+
</head>
|
|
134
|
+
<body>
|
|
135
|
+
<main>
|
|
136
|
+
<h1>${theme.emoji} ${name}</h1>
|
|
137
|
+
<p class="tagline">${escapeHtml(theme.narrativeIntro)}</p>
|
|
138
|
+
<canvas id="game" width="480" height="640"></canvas>
|
|
139
|
+
<p class="hint">${escapeHtml(str(theme, 'controlsHint'))} ${escapeHtml(str(theme, 'startHint'))}</p>
|
|
140
|
+
</main>
|
|
141
|
+
<script src="game.js"></script>
|
|
142
|
+
</body>
|
|
143
|
+
</html>
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
function styleCss(theme, prettyName) {
|
|
147
|
+
return `/* Styles for ${cleanName(prettyName)}. The colors come from your theme. */
|
|
148
|
+
:root {
|
|
149
|
+
--bg: ${theme.palette.bg};
|
|
150
|
+
--fg: ${theme.palette.fg};
|
|
151
|
+
--accent: ${theme.palette.accent};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
* {
|
|
155
|
+
box-sizing: border-box;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
body {
|
|
159
|
+
margin: 0;
|
|
160
|
+
min-height: 100vh;
|
|
161
|
+
display: flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
justify-content: center;
|
|
164
|
+
background: var(--bg);
|
|
165
|
+
color: var(--fg);
|
|
166
|
+
font-family: "Trebuchet MS", "Segoe UI", sans-serif;
|
|
167
|
+
text-align: center;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
main {
|
|
171
|
+
padding: 16px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
h1 {
|
|
175
|
+
margin: 8px 0;
|
|
176
|
+
font-size: 1.6rem;
|
|
177
|
+
letter-spacing: 1px;
|
|
178
|
+
color: var(--accent);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.tagline {
|
|
182
|
+
max-width: 480px;
|
|
183
|
+
margin: 0 auto 12px;
|
|
184
|
+
opacity: 0.85;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
canvas {
|
|
188
|
+
width: min(92vw, 480px);
|
|
189
|
+
height: auto;
|
|
190
|
+
border: 3px solid var(--accent);
|
|
191
|
+
border-radius: 12px;
|
|
192
|
+
background: var(--bg);
|
|
193
|
+
touch-action: none;
|
|
194
|
+
box-shadow: 0 0 28px rgba(0, 0, 0, 0.5);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.hint {
|
|
198
|
+
font-size: 0.9rem;
|
|
199
|
+
opacity: 0.8;
|
|
200
|
+
}
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
function gameJs(theme, prettyName) {
|
|
204
|
+
const name = cleanName(prettyName);
|
|
205
|
+
return `// ${name} (a Sky Dash game)
|
|
206
|
+
// You steer the player. Dodge the ${str(theme, 'obstacleWord')}. Grab ${str(theme, 'bonusWord')} for points.
|
|
207
|
+
// You and Termi can change anything in this file. Have fun!
|
|
208
|
+
|
|
209
|
+
const THEME = ${themeBlock(theme)};
|
|
210
|
+
|
|
211
|
+
const GAME_NAME = ${JSON.stringify(name)};
|
|
212
|
+
|
|
213
|
+
// --- Canvas setup ---
|
|
214
|
+
const canvas = document.getElementById("game");
|
|
215
|
+
const ctx = canvas.getContext("2d");
|
|
216
|
+
const W = canvas.width;
|
|
217
|
+
const H = canvas.height;
|
|
218
|
+
|
|
219
|
+
// --- Game settings (easy to tweak!) ---
|
|
220
|
+
const WIN_SCORE = 150; // reach this score to win
|
|
221
|
+
const PLAYER_SPEED = 5; // how fast you move
|
|
222
|
+
const START_FALL_SPEED = 2.4; // how fast things fall at the start
|
|
223
|
+
const SPEED_RAMP = 0.0011; // how much faster every frame
|
|
224
|
+
|
|
225
|
+
// --- Game state ---
|
|
226
|
+
let player;
|
|
227
|
+
let obstacles;
|
|
228
|
+
let bonuses;
|
|
229
|
+
let score;
|
|
230
|
+
let fallSpeed;
|
|
231
|
+
let spawnGap;
|
|
232
|
+
let frame;
|
|
233
|
+
let state = "start"; // "start", "play", "win", or "lose"
|
|
234
|
+
const keysDown = {};
|
|
235
|
+
let touchTarget = null;
|
|
236
|
+
|
|
237
|
+
function resetGame() {
|
|
238
|
+
player = { x: W / 2, y: H - 80, size: 36 };
|
|
239
|
+
obstacles = [];
|
|
240
|
+
bonuses = [];
|
|
241
|
+
score = 0;
|
|
242
|
+
fallSpeed = START_FALL_SPEED;
|
|
243
|
+
spawnGap = 55;
|
|
244
|
+
frame = 0;
|
|
245
|
+
}
|
|
246
|
+
resetGame();
|
|
247
|
+
|
|
248
|
+
// --- Keyboard controls: arrow keys and WASD ---
|
|
249
|
+
window.addEventListener("keydown", function (event) {
|
|
250
|
+
const key = event.key.toLowerCase();
|
|
251
|
+
keysDown[key] = true;
|
|
252
|
+
const gameKeys = ["arrowleft", "arrowright", "arrowup", "arrowdown", " "];
|
|
253
|
+
if (gameKeys.includes(key)) event.preventDefault();
|
|
254
|
+
if (state !== "play") startOrRetry(key);
|
|
255
|
+
});
|
|
256
|
+
window.addEventListener("keyup", function (event) {
|
|
257
|
+
keysDown[event.key.toLowerCase()] = false;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
function startOrRetry(key) {
|
|
261
|
+
// Any key starts the game. R, Space, or Enter restarts it.
|
|
262
|
+
const restartKeys = ["r", " ", "enter"];
|
|
263
|
+
if (state === "start" || restartKeys.includes(key)) {
|
|
264
|
+
resetGame();
|
|
265
|
+
state = "play";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// --- Touch controls: slide your finger to steer ---
|
|
270
|
+
canvas.addEventListener("touchstart", function (event) {
|
|
271
|
+
event.preventDefault();
|
|
272
|
+
if (state !== "play") {
|
|
273
|
+
startOrRetry("r");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
touchTarget = touchPoint(event);
|
|
277
|
+
}, { passive: false });
|
|
278
|
+
canvas.addEventListener("touchmove", function (event) {
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
touchTarget = touchPoint(event);
|
|
281
|
+
}, { passive: false });
|
|
282
|
+
canvas.addEventListener("touchend", function () {
|
|
283
|
+
touchTarget = null;
|
|
284
|
+
});
|
|
285
|
+
canvas.addEventListener("mousedown", function () {
|
|
286
|
+
if (state !== "play") startOrRetry("r");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
function touchPoint(event) {
|
|
290
|
+
// Turn a finger spot on the screen into a spot on the canvas.
|
|
291
|
+
const rect = canvas.getBoundingClientRect();
|
|
292
|
+
const touch = event.touches[0];
|
|
293
|
+
return {
|
|
294
|
+
x: (touch.clientX - rect.left) * (W / rect.width),
|
|
295
|
+
y: (touch.clientY - rect.top) * (H / rect.height),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --- Game logic ---
|
|
300
|
+
function update() {
|
|
301
|
+
frame = frame + 1;
|
|
302
|
+
|
|
303
|
+
// Your score climbs while you survive.
|
|
304
|
+
if (frame % 6 === 0) score = score + 1;
|
|
305
|
+
|
|
306
|
+
// The game slowly speeds up.
|
|
307
|
+
fallSpeed = fallSpeed + SPEED_RAMP;
|
|
308
|
+
if (spawnGap > 24) spawnGap = spawnGap - 0.012;
|
|
309
|
+
|
|
310
|
+
movePlayer();
|
|
311
|
+
|
|
312
|
+
// Drop in new things to dodge and grab.
|
|
313
|
+
if (frame % Math.floor(spawnGap) === 0) spawnObstacle();
|
|
314
|
+
if (frame % 240 === 120) spawnBonus();
|
|
315
|
+
|
|
316
|
+
moveThings(obstacles);
|
|
317
|
+
moveThings(bonuses);
|
|
318
|
+
|
|
319
|
+
// Did something hit us?
|
|
320
|
+
for (const thing of obstacles) {
|
|
321
|
+
if (hits(player, thing)) {
|
|
322
|
+
state = "lose";
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Did we grab a bonus? Loop backwards so removing is safe.
|
|
328
|
+
for (let i = bonuses.length - 1; i >= 0; i--) {
|
|
329
|
+
if (hits(player, bonuses[i])) {
|
|
330
|
+
score = score + 10;
|
|
331
|
+
bonuses.splice(i, 1);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (score >= WIN_SCORE) state = "win";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function movePlayer() {
|
|
339
|
+
let dx = 0;
|
|
340
|
+
let dy = 0;
|
|
341
|
+
if (keysDown["arrowleft"] || keysDown["a"]) dx = dx - 1;
|
|
342
|
+
if (keysDown["arrowright"] || keysDown["d"]) dx = dx + 1;
|
|
343
|
+
if (keysDown["arrowup"] || keysDown["w"]) dy = dy - 1;
|
|
344
|
+
if (keysDown["arrowdown"] || keysDown["s"]) dy = dy + 1;
|
|
345
|
+
if (touchTarget) {
|
|
346
|
+
// Glide toward your finger.
|
|
347
|
+
if (touchTarget.x > player.x + 6) dx = 1;
|
|
348
|
+
if (touchTarget.x < player.x - 6) dx = -1;
|
|
349
|
+
if (touchTarget.y > player.y + 6) dy = 1;
|
|
350
|
+
if (touchTarget.y < player.y - 6) dy = -1;
|
|
351
|
+
}
|
|
352
|
+
player.x = clamp(player.x + dx * PLAYER_SPEED, 24, W - 24);
|
|
353
|
+
player.y = clamp(player.y + dy * PLAYER_SPEED, H / 2, H - 24);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function spawnObstacle() {
|
|
357
|
+
obstacles.push({
|
|
358
|
+
x: 24 + Math.random() * (W - 48),
|
|
359
|
+
y: -40,
|
|
360
|
+
size: 26 + Math.random() * 18,
|
|
361
|
+
drift: (Math.random() - 0.5) * 1.6,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function spawnBonus() {
|
|
366
|
+
bonuses.push({
|
|
367
|
+
x: 24 + Math.random() * (W - 48),
|
|
368
|
+
y: -40,
|
|
369
|
+
size: 26,
|
|
370
|
+
drift: 0,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function moveThings(list) {
|
|
375
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
376
|
+
const thing = list[i];
|
|
377
|
+
thing.y = thing.y + fallSpeed + thing.size * 0.02;
|
|
378
|
+
thing.x = thing.x + thing.drift;
|
|
379
|
+
if (thing.y > H + 60) list.splice(i, 1);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function hits(a, b) {
|
|
384
|
+
// True when two things are close enough to touch.
|
|
385
|
+
const gap = (a.size + b.size) * 0.42;
|
|
386
|
+
return Math.abs(a.x - b.x) < gap && Math.abs(a.y - b.y) < gap;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function clamp(value, low, high) {
|
|
390
|
+
return Math.min(high, Math.max(low, value));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// --- Drawing ---
|
|
394
|
+
function draw() {
|
|
395
|
+
ctx.fillStyle = THEME.palette.bg;
|
|
396
|
+
ctx.fillRect(0, 0, W, H);
|
|
397
|
+
drawBackgroundDots();
|
|
398
|
+
|
|
399
|
+
for (const thing of obstacles) drawGlyph(THEME.glyphs.obstacle, thing.x, thing.y, thing.size);
|
|
400
|
+
for (const thing of bonuses) drawGlyph(THEME.glyphs.bonus, thing.x, thing.y, thing.size);
|
|
401
|
+
drawGlyph(THEME.glyphs.player, player.x, player.y, player.size);
|
|
402
|
+
|
|
403
|
+
// Score in the corner.
|
|
404
|
+
ctx.fillStyle = THEME.palette.fg;
|
|
405
|
+
ctx.font = "bold 22px sans-serif";
|
|
406
|
+
ctx.textAlign = "left";
|
|
407
|
+
ctx.textBaseline = "alphabetic";
|
|
408
|
+
ctx.fillText(THEME.strings.scoreLabel + ": " + score, 14, 32);
|
|
409
|
+
|
|
410
|
+
if (state === "start") drawCard(GAME_NAME, THEME.narrative, THEME.strings.startHint, false);
|
|
411
|
+
if (state === "lose") drawCard(THEME.strings.loseTitle, THEME.strings.loseLine, THEME.strings.retryHint, true);
|
|
412
|
+
if (state === "win") drawCard(THEME.strings.winTitle, THEME.strings.winLine, THEME.strings.retryHint, true);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function drawBackgroundDots() {
|
|
416
|
+
// Little drifting dots make it feel like you are moving.
|
|
417
|
+
ctx.fillStyle = THEME.palette.accent;
|
|
418
|
+
ctx.globalAlpha = 0.25;
|
|
419
|
+
for (let i = 0; i < 26; i++) {
|
|
420
|
+
const x = (i * 97) % W;
|
|
421
|
+
const y = (i * 173 + frame * 1.5) % H;
|
|
422
|
+
ctx.fillRect(x, y, 3, 3);
|
|
423
|
+
}
|
|
424
|
+
ctx.globalAlpha = 1;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function drawGlyph(glyph, x, y, size) {
|
|
428
|
+
ctx.font = size + "px serif";
|
|
429
|
+
ctx.textAlign = "center";
|
|
430
|
+
ctx.textBaseline = "middle";
|
|
431
|
+
ctx.fillText(glyph, x, y);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function drawCard(title, body, hint, showScore) {
|
|
435
|
+
// A friendly message card in the middle of the screen.
|
|
436
|
+
const top = H / 2 - 150;
|
|
437
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.6)";
|
|
438
|
+
ctx.fillRect(30, top, W - 60, 300);
|
|
439
|
+
ctx.strokeStyle = THEME.palette.accent;
|
|
440
|
+
ctx.lineWidth = 3;
|
|
441
|
+
ctx.strokeRect(30, top, W - 60, 300);
|
|
442
|
+
|
|
443
|
+
ctx.textAlign = "center";
|
|
444
|
+
ctx.textBaseline = "alphabetic";
|
|
445
|
+
|
|
446
|
+
ctx.fillStyle = THEME.palette.accent;
|
|
447
|
+
ctx.font = "bold 30px sans-serif";
|
|
448
|
+
ctx.fillText(title, W / 2, top + 56);
|
|
449
|
+
|
|
450
|
+
ctx.fillStyle = THEME.palette.fg;
|
|
451
|
+
ctx.font = "19px sans-serif";
|
|
452
|
+
const lines = wrapText(body, 38);
|
|
453
|
+
for (let i = 0; i < lines.length; i++) {
|
|
454
|
+
ctx.fillText(lines[i], W / 2, top + 104 + i * 26);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (showScore) {
|
|
458
|
+
ctx.font = "bold 22px sans-serif";
|
|
459
|
+
ctx.fillText(THEME.strings.scoreLabel + ": " + score, W / 2, top + 228);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
ctx.fillStyle = THEME.palette.accent;
|
|
463
|
+
ctx.font = "16px sans-serif";
|
|
464
|
+
ctx.fillText(hint, W / 2, top + 268);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function wrapText(text, maxChars) {
|
|
468
|
+
// Splits long text into short lines that fit the card.
|
|
469
|
+
const words = text.split(" ");
|
|
470
|
+
const lines = [];
|
|
471
|
+
let line = "";
|
|
472
|
+
for (const word of words) {
|
|
473
|
+
if ((line + " " + word).trim().length > maxChars) {
|
|
474
|
+
lines.push(line.trim());
|
|
475
|
+
line = word;
|
|
476
|
+
} else {
|
|
477
|
+
line = line + " " + word;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (line.trim().length > 0) lines.push(line.trim());
|
|
481
|
+
return lines;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// --- Main loop ---
|
|
485
|
+
function loop() {
|
|
486
|
+
if (state === "play") update();
|
|
487
|
+
draw();
|
|
488
|
+
requestAnimationFrame(loop);
|
|
489
|
+
}
|
|
490
|
+
loop();
|
|
491
|
+
`;
|
|
492
|
+
}
|
|
493
|
+
export const gamesScaffold = {
|
|
494
|
+
id: 'games',
|
|
495
|
+
label: 'Games',
|
|
496
|
+
emoji: '🎮',
|
|
497
|
+
ageNote: 'A quick dodge game. A great first project for ages 9 and up.',
|
|
498
|
+
themes,
|
|
499
|
+
files(theme, prettyName) {
|
|
500
|
+
return {
|
|
501
|
+
'index.html': indexHtml(theme, prettyName),
|
|
502
|
+
'style.css': styleCss(theme, prettyName),
|
|
503
|
+
'game.js': gameJs(theme, prettyName),
|
|
504
|
+
};
|
|
505
|
+
},
|
|
506
|
+
starterPrompts(theme) {
|
|
507
|
+
return [
|
|
508
|
+
'give me 3 lives instead of 1',
|
|
509
|
+
'make the ' + str(theme, 'obstacleWord') + ' spin as they fall',
|
|
510
|
+
'add a power up that makes me super fast',
|
|
511
|
+
'make the game get harder after 50 points',
|
|
512
|
+
'change the background color when i grab ' + str(theme, 'bonusWord'),
|
|
513
|
+
];
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
export default gamesScaffold;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { gamesScaffold } from './games.js';
|
|
2
|
+
import { bigGamesScaffold } from './biggames.js';
|
|
3
|
+
import { artScaffold } from './art.js';
|
|
4
|
+
import { musicScaffold } from './music.js';
|
|
5
|
+
import { petsScaffold } from './pets.js';
|
|
6
|
+
import { storiesScaffold } from './stories.js';
|
|
7
|
+
import { quizzesScaffold } from './quizzes.js';
|
|
8
|
+
import { websitesScaffold } from './websites.js';
|
|
9
|
+
import { charactersScaffold } from './characters.js';
|
|
10
|
+
/** All project types, in home-menu order. Games lead: they are the front door. */
|
|
11
|
+
export const scaffolds = [
|
|
12
|
+
gamesScaffold,
|
|
13
|
+
bigGamesScaffold,
|
|
14
|
+
artScaffold,
|
|
15
|
+
musicScaffold,
|
|
16
|
+
petsScaffold,
|
|
17
|
+
storiesScaffold,
|
|
18
|
+
quizzesScaffold,
|
|
19
|
+
websitesScaffold,
|
|
20
|
+
charactersScaffold,
|
|
21
|
+
];
|
|
22
|
+
export function scaffoldById(id) {
|
|
23
|
+
return scaffolds.find((s) => s.id === id);
|
|
24
|
+
}
|