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,554 @@
|
|
|
1
|
+
// Engine vendored from the npm package kaplay@3001.0.19 (see vendor/KAPLAY-LICENSE.txt).
|
|
2
|
+
/**
|
|
3
|
+
* "Big Games" scaffold: a KAPLAY platformer with two short levels.
|
|
4
|
+
* The engine ships as a local file (vendor/kaplay.mjs), so the game
|
|
5
|
+
* needs zero network access. Theme data lands in game.js as a const
|
|
6
|
+
* THEME block that kids and the AI can tweak easily.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
const themes = [
|
|
10
|
+
{
|
|
11
|
+
id: 'castle-quest',
|
|
12
|
+
label: 'Castle Quest',
|
|
13
|
+
emoji: 'đ°',
|
|
14
|
+
palette: { bg: '#2b2150', fg: '#f3ecff', accent: '#f5b83d' },
|
|
15
|
+
glyphs: { player: 'đ¤ē', coin: 'đĒ', hazard: 'đ', flag: 'đŠ', deco: 'âī¸' },
|
|
16
|
+
strings: {
|
|
17
|
+
collectWord: 'gold coins',
|
|
18
|
+
hazardWord: 'dragon',
|
|
19
|
+
scoreLabel: 'Gold',
|
|
20
|
+
levelLabel: 'Level',
|
|
21
|
+
goalHint: 'Reach the flag!',
|
|
22
|
+
startHint: 'Press any key or tap to start your quest.',
|
|
23
|
+
controlsHint: 'Run with arrow keys or WASD. Jump with Space, Up, or W.',
|
|
24
|
+
touchHint: 'On touch: hold the sides to run. Tap the middle to jump.',
|
|
25
|
+
retryHint: 'Press R, Space, or tap to try again.',
|
|
26
|
+
winTitle: 'Quest Complete!',
|
|
27
|
+
winLine: 'You reached the castle gate. The whole kingdom cheers for you.',
|
|
28
|
+
loseTitle: 'Dragon Huff!',
|
|
29
|
+
loseLine: 'The dragon huffed at you! Brave knights never quit. Try again?',
|
|
30
|
+
},
|
|
31
|
+
narrativeIntro: 'You are a brave knight on a quest. Grab the gold coins. Slip past the dragon. Reach the flag in both levels.',
|
|
32
|
+
nonViolent: true,
|
|
33
|
+
nonCompetitive: true,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'blocky-mine',
|
|
37
|
+
label: 'Blocky Mine World',
|
|
38
|
+
emoji: 'âī¸',
|
|
39
|
+
palette: { bg: '#26211b', fg: '#f0ead8', accent: '#5fbf4f' },
|
|
40
|
+
glyphs: { player: 'đˇ', coin: 'đ', hazard: 'đ', flag: 'đŠ', deco: 'đǍ' },
|
|
41
|
+
strings: {
|
|
42
|
+
collectWord: 'gems',
|
|
43
|
+
hazardWord: 'cave snake',
|
|
44
|
+
scoreLabel: 'Gems',
|
|
45
|
+
levelLabel: 'Level',
|
|
46
|
+
goalHint: 'Reach the flag!',
|
|
47
|
+
startHint: 'Press any key or tap to start digging.',
|
|
48
|
+
controlsHint: 'Run with arrow keys or WASD. Jump with Space, Up, or W.',
|
|
49
|
+
touchHint: 'On touch: hold the sides to run. Tap the middle to jump.',
|
|
50
|
+
retryHint: 'Press R, Space, or tap to try again.',
|
|
51
|
+
winTitle: 'Mine Cleared!',
|
|
52
|
+
winLine: 'You explored every tunnel. Your gem bag sparkles all the way home.',
|
|
53
|
+
loseTitle: 'Sss!',
|
|
54
|
+
loseLine: 'The cave snake surprised you. Grab your helmet and dig back in?',
|
|
55
|
+
},
|
|
56
|
+
narrativeIntro: 'Deep in the blocky mine. Collect the shiny gems. Watch out for the cave snake. Find the flag to climb out.',
|
|
57
|
+
nonViolent: true,
|
|
58
|
+
nonCompetitive: true,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'haunted-house',
|
|
62
|
+
label: 'Haunted House',
|
|
63
|
+
emoji: 'đģ',
|
|
64
|
+
palette: { bg: '#191325', fg: '#e8e3f2', accent: '#9b6df2' },
|
|
65
|
+
glyphs: { player: 'đ§', coin: 'đŽ', hazard: 'đģ', flag: 'đĒ', deco: 'đ¸ī¸' },
|
|
66
|
+
strings: {
|
|
67
|
+
collectWord: 'glow orbs',
|
|
68
|
+
hazardWord: 'ghost',
|
|
69
|
+
scoreLabel: 'Orbs',
|
|
70
|
+
levelLabel: 'Level',
|
|
71
|
+
goalHint: 'Find the door!',
|
|
72
|
+
startHint: 'Press any key or tap to tiptoe inside.',
|
|
73
|
+
controlsHint: 'Run with arrow keys or WASD. Jump with Space, Up, or W.',
|
|
74
|
+
touchHint: 'On touch: hold the sides to run. Tap the middle to jump.',
|
|
75
|
+
retryHint: 'Press R, Space, or tap to try again.',
|
|
76
|
+
winTitle: 'You Escaped!',
|
|
77
|
+
winLine: 'You found the way out. The old house giggles a friendly goodbye.',
|
|
78
|
+
loseTitle: 'Boo!',
|
|
79
|
+
loseLine: 'The ghost spooked you. Take a deep breath and tiptoe back in?',
|
|
80
|
+
},
|
|
81
|
+
narrativeIntro: 'You are inside a haunted house. Collect the glow orbs. Sneak past the ghost. Find the door in both levels.',
|
|
82
|
+
nonViolent: false,
|
|
83
|
+
nonCompetitive: true,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'midnight-wolf',
|
|
87
|
+
label: 'Midnight Wolf Pack',
|
|
88
|
+
emoji: 'đē',
|
|
89
|
+
palette: { bg: '#0e1626', fg: '#e6eefc', accent: '#8fb7ff' },
|
|
90
|
+
glyphs: { player: 'đē', coin: 'đ', hazard: 'đ', flag: 'đž', deco: 'đ˛' },
|
|
91
|
+
strings: {
|
|
92
|
+
collectWord: 'moonstones',
|
|
93
|
+
hazardWord: 'grumpy boar',
|
|
94
|
+
scoreLabel: 'Moons',
|
|
95
|
+
levelLabel: 'Level',
|
|
96
|
+
goalHint: 'Follow the paw prints!',
|
|
97
|
+
startHint: 'Press any key or tap to start running.',
|
|
98
|
+
controlsHint: 'Run with arrow keys or WASD. Jump with Space, Up, or W.',
|
|
99
|
+
touchHint: 'On touch: hold the sides to run. Tap the middle to jump.',
|
|
100
|
+
retryHint: 'Press R, Space, or tap to run again.',
|
|
101
|
+
winTitle: 'Awooo!',
|
|
102
|
+
winLine: 'You found your pack under the full moon. They howl with joy.',
|
|
103
|
+
loseTitle: 'Bumped!',
|
|
104
|
+
loseLine: 'The grumpy boar bumped you. Shake your fur and run again?',
|
|
105
|
+
},
|
|
106
|
+
narrativeIntro: 'You are a young wolf far from home. Collect moonstones. Dodge the grumpy boar. Follow the paw prints to your pack.',
|
|
107
|
+
nonViolent: true,
|
|
108
|
+
nonCompetitive: true,
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
/** Builds the JSON for the const THEME block at the top of game.js. */
|
|
112
|
+
function themeBlock(theme) {
|
|
113
|
+
return JSON.stringify({
|
|
114
|
+
id: theme.id,
|
|
115
|
+
label: theme.label,
|
|
116
|
+
palette: theme.palette,
|
|
117
|
+
glyphs: theme.glyphs,
|
|
118
|
+
strings: theme.strings,
|
|
119
|
+
narrative: theme.narrativeIntro,
|
|
120
|
+
}, null, 2);
|
|
121
|
+
}
|
|
122
|
+
/** Makes a name safe to drop into HTML text. */
|
|
123
|
+
function escapeHtml(text) {
|
|
124
|
+
return text
|
|
125
|
+
.replace(/&/g, '&')
|
|
126
|
+
.replace(/</g, '<')
|
|
127
|
+
.replace(/>/g, '>')
|
|
128
|
+
.replace(/"/g, '"')
|
|
129
|
+
.replace(/'/g, ''');
|
|
130
|
+
}
|
|
131
|
+
/** Keeps a kid-typed name to one tidy line. */
|
|
132
|
+
function cleanName(name) {
|
|
133
|
+
const tidy = name.replace(/[\r\n\t]+/g, ' ').replace(/ {2,}/g, ' ').trim();
|
|
134
|
+
return tidy.length > 0 ? tidy : 'My Big Game';
|
|
135
|
+
}
|
|
136
|
+
function str(theme, key) {
|
|
137
|
+
return theme.strings[key] ?? '';
|
|
138
|
+
}
|
|
139
|
+
function indexHtml(theme, prettyName) {
|
|
140
|
+
const name = escapeHtml(cleanName(prettyName));
|
|
141
|
+
return `<!DOCTYPE html>
|
|
142
|
+
<html lang="en">
|
|
143
|
+
<head>
|
|
144
|
+
<meta charset="UTF-8" />
|
|
145
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
146
|
+
<title>${name}</title>
|
|
147
|
+
<link rel="stylesheet" href="style.css" />
|
|
148
|
+
</head>
|
|
149
|
+
<body>
|
|
150
|
+
<main>
|
|
151
|
+
<h1>${theme.emoji} ${name}</h1>
|
|
152
|
+
<canvas id="game"></canvas>
|
|
153
|
+
<p class="hint">${escapeHtml(str(theme, 'controlsHint'))} ${escapeHtml(str(theme, 'touchHint'))}</p>
|
|
154
|
+
</main>
|
|
155
|
+
<script type="module" src="game.js"></script>
|
|
156
|
+
</body>
|
|
157
|
+
</html>
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
160
|
+
function styleCss(theme, prettyName) {
|
|
161
|
+
return `/* Styles for ${cleanName(prettyName)}. The colors come from your theme. */
|
|
162
|
+
:root {
|
|
163
|
+
--bg: ${theme.palette.bg};
|
|
164
|
+
--fg: ${theme.palette.fg};
|
|
165
|
+
--accent: ${theme.palette.accent};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
* {
|
|
169
|
+
box-sizing: border-box;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
body {
|
|
173
|
+
margin: 0;
|
|
174
|
+
min-height: 100vh;
|
|
175
|
+
display: flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
justify-content: center;
|
|
178
|
+
background: var(--bg);
|
|
179
|
+
color: var(--fg);
|
|
180
|
+
font-family: "Trebuchet MS", "Segoe UI", sans-serif;
|
|
181
|
+
text-align: center;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
main {
|
|
185
|
+
padding: 16px;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
h1 {
|
|
189
|
+
margin: 8px 0 12px;
|
|
190
|
+
font-size: 1.6rem;
|
|
191
|
+
letter-spacing: 1px;
|
|
192
|
+
color: var(--accent);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
canvas {
|
|
196
|
+
width: min(94vw, 880px);
|
|
197
|
+
aspect-ratio: 22 / 13;
|
|
198
|
+
border: 3px solid var(--accent);
|
|
199
|
+
border-radius: 12px;
|
|
200
|
+
background: var(--bg);
|
|
201
|
+
touch-action: none;
|
|
202
|
+
box-shadow: 0 0 28px rgba(0, 0, 0, 0.5);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.hint {
|
|
206
|
+
font-size: 0.9rem;
|
|
207
|
+
opacity: 0.8;
|
|
208
|
+
}
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
function gameJs(theme, prettyName) {
|
|
212
|
+
const name = cleanName(prettyName);
|
|
213
|
+
return `// ${name} (a platform adventure)
|
|
214
|
+
// Run and jump. Grab ${str(theme, 'collectWord')}. Dodge the ${str(theme, 'hazardWord')}. Reach the goal!
|
|
215
|
+
// You and Termi can change anything in this file. Try the starter ideas!
|
|
216
|
+
// The game engine is the local file kaplay.mjs. No internet needed.
|
|
217
|
+
|
|
218
|
+
import kaplay from "./kaplay.mjs";
|
|
219
|
+
|
|
220
|
+
const THEME = ${themeBlock(theme)};
|
|
221
|
+
|
|
222
|
+
const GAME_NAME = ${JSON.stringify(name)};
|
|
223
|
+
|
|
224
|
+
// --- Engine setup ---
|
|
225
|
+
const TILE = 48; // size of one level block
|
|
226
|
+
const MOVE_SPEED = 220; // how fast you run
|
|
227
|
+
const JUMP_POWER = 700; // how high you jump
|
|
228
|
+
|
|
229
|
+
const k = kaplay({
|
|
230
|
+
canvas: document.getElementById("game"),
|
|
231
|
+
width: 880,
|
|
232
|
+
height: 520,
|
|
233
|
+
stretch: true,
|
|
234
|
+
letterbox: true,
|
|
235
|
+
background: THEME.palette.bg,
|
|
236
|
+
global: false,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
k.setGravity(1400);
|
|
240
|
+
|
|
241
|
+
const accent = k.Color.fromHex(THEME.palette.accent);
|
|
242
|
+
const fgColor = k.Color.fromHex(THEME.palette.fg);
|
|
243
|
+
|
|
244
|
+
// --- The levels ---
|
|
245
|
+
// "=" is solid ground. "o" is something to collect.
|
|
246
|
+
// "^" is where the hazard starts. "F" is the goal.
|
|
247
|
+
const LEVELS = [
|
|
248
|
+
[
|
|
249
|
+
" ",
|
|
250
|
+
" ",
|
|
251
|
+
" oo ",
|
|
252
|
+
" ==== o ",
|
|
253
|
+
" o o === ",
|
|
254
|
+
" ^ F ",
|
|
255
|
+
"========================",
|
|
256
|
+
],
|
|
257
|
+
[
|
|
258
|
+
" ",
|
|
259
|
+
" oo o ",
|
|
260
|
+
" ==== === ",
|
|
261
|
+
" o o ",
|
|
262
|
+
" === === ==== ^ F ",
|
|
263
|
+
"============= ==============",
|
|
264
|
+
],
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
// --- The start screen ---
|
|
268
|
+
k.scene("start", function () {
|
|
269
|
+
k.add([
|
|
270
|
+
k.text(GAME_NAME, { size: 44 }),
|
|
271
|
+
k.pos(k.width() / 2, 110),
|
|
272
|
+
k.anchor("center"),
|
|
273
|
+
k.color(accent),
|
|
274
|
+
]);
|
|
275
|
+
k.add([
|
|
276
|
+
k.text(THEME.narrative, { size: 22, width: 660, align: "center" }),
|
|
277
|
+
k.pos(k.width() / 2, 230),
|
|
278
|
+
k.anchor("center"),
|
|
279
|
+
k.color(fgColor),
|
|
280
|
+
]);
|
|
281
|
+
k.add([
|
|
282
|
+
k.text(THEME.strings.controlsHint + " " + THEME.strings.touchHint, { size: 17, width: 720, align: "center" }),
|
|
283
|
+
k.pos(k.width() / 2, 350),
|
|
284
|
+
k.anchor("center"),
|
|
285
|
+
k.color(fgColor),
|
|
286
|
+
k.opacity(0.8),
|
|
287
|
+
]);
|
|
288
|
+
k.add([
|
|
289
|
+
k.text(THEME.strings.startHint, { size: 22 }),
|
|
290
|
+
k.pos(k.width() / 2, 440),
|
|
291
|
+
k.anchor("center"),
|
|
292
|
+
k.color(accent),
|
|
293
|
+
]);
|
|
294
|
+
function begin() {
|
|
295
|
+
k.go("game", { level: 0, score: 0 });
|
|
296
|
+
}
|
|
297
|
+
k.onKeyPress(begin);
|
|
298
|
+
k.onMousePress(begin);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// --- The main game ---
|
|
302
|
+
k.scene("game", function (data) {
|
|
303
|
+
const rows = LEVELS[data.level];
|
|
304
|
+
const levelWidth = rows[0].length * TILE;
|
|
305
|
+
const levelTop = k.height() - rows.length * TILE;
|
|
306
|
+
let score = data.score;
|
|
307
|
+
|
|
308
|
+
k.addLevel(rows, {
|
|
309
|
+
tileWidth: TILE,
|
|
310
|
+
tileHeight: TILE,
|
|
311
|
+
pos: k.vec2(0, levelTop),
|
|
312
|
+
tiles: {
|
|
313
|
+
"=": function () {
|
|
314
|
+
return [
|
|
315
|
+
k.rect(TILE, TILE),
|
|
316
|
+
k.color(accent),
|
|
317
|
+
k.outline(3, k.Color.fromHex(THEME.palette.bg)),
|
|
318
|
+
k.area(),
|
|
319
|
+
k.body({ isStatic: true }),
|
|
320
|
+
"ground",
|
|
321
|
+
];
|
|
322
|
+
},
|
|
323
|
+
"o": function () {
|
|
324
|
+
return [k.text(THEME.glyphs.coin, { size: 30 }), k.area(), "coin"];
|
|
325
|
+
},
|
|
326
|
+
"^": function () {
|
|
327
|
+
return [k.text(THEME.glyphs.hazard, { size: 34 }), k.area(), "hazard"];
|
|
328
|
+
},
|
|
329
|
+
"F": function () {
|
|
330
|
+
return [k.text(THEME.glyphs.flag, { size: 40 }), k.area(), "flag"];
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Soft background decorations.
|
|
336
|
+
for (let i = 0; i < Math.floor(levelWidth / 300); i++) {
|
|
337
|
+
k.add([
|
|
338
|
+
k.text(THEME.glyphs.deco, { size: 40 }),
|
|
339
|
+
k.pos(i * 300 + 140, levelTop + 30 + (i % 3) * 50),
|
|
340
|
+
k.opacity(0.35),
|
|
341
|
+
k.z(-10),
|
|
342
|
+
]);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// The player drops in near the left edge.
|
|
346
|
+
const player = k.add([
|
|
347
|
+
k.text(THEME.glyphs.player, { size: 36 }),
|
|
348
|
+
k.pos(TILE * 1.5, levelTop),
|
|
349
|
+
k.anchor("center"),
|
|
350
|
+
k.area(),
|
|
351
|
+
k.body(),
|
|
352
|
+
k.z(10),
|
|
353
|
+
"player",
|
|
354
|
+
]);
|
|
355
|
+
|
|
356
|
+
// Score and level signs that stay on screen.
|
|
357
|
+
const scoreText = k.add([
|
|
358
|
+
k.text(THEME.strings.scoreLabel + ": " + score, { size: 22 }),
|
|
359
|
+
k.pos(14, 12),
|
|
360
|
+
k.color(fgColor),
|
|
361
|
+
k.fixed(),
|
|
362
|
+
k.z(100),
|
|
363
|
+
]);
|
|
364
|
+
k.add([
|
|
365
|
+
k.text(THEME.strings.levelLabel + " " + (data.level + 1) + " of " + LEVELS.length, { size: 22 }),
|
|
366
|
+
k.pos(k.width() - 14, 12),
|
|
367
|
+
k.anchor("topright"),
|
|
368
|
+
k.color(fgColor),
|
|
369
|
+
k.fixed(),
|
|
370
|
+
k.z(100),
|
|
371
|
+
]);
|
|
372
|
+
const goalSign = k.add([
|
|
373
|
+
k.text(THEME.strings.goalHint, { size: 22 }),
|
|
374
|
+
k.pos(k.width() / 2, 52),
|
|
375
|
+
k.anchor("center"),
|
|
376
|
+
k.color(accent),
|
|
377
|
+
k.fixed(),
|
|
378
|
+
k.z(100),
|
|
379
|
+
]);
|
|
380
|
+
k.wait(3, function () {
|
|
381
|
+
k.destroy(goalSign);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// --- Keyboard controls: arrows and WASD ---
|
|
385
|
+
function tryJump() {
|
|
386
|
+
if (player.isGrounded()) player.jump(JUMP_POWER);
|
|
387
|
+
}
|
|
388
|
+
k.onKeyDown("left", function () { player.move(-MOVE_SPEED, 0); });
|
|
389
|
+
k.onKeyDown("a", function () { player.move(-MOVE_SPEED, 0); });
|
|
390
|
+
k.onKeyDown("right", function () { player.move(MOVE_SPEED, 0); });
|
|
391
|
+
k.onKeyDown("d", function () { player.move(MOVE_SPEED, 0); });
|
|
392
|
+
k.onKeyPress("space", tryJump);
|
|
393
|
+
k.onKeyPress("up", tryJump);
|
|
394
|
+
k.onKeyPress("w", tryJump);
|
|
395
|
+
|
|
396
|
+
// --- Touch controls: hold the sides to run, tap the middle to jump ---
|
|
397
|
+
let touchDir = 0;
|
|
398
|
+
k.onMousePress(function () {
|
|
399
|
+
const x = k.mousePos().x;
|
|
400
|
+
if (x < k.width() / 3) touchDir = -1;
|
|
401
|
+
else if (x > (k.width() * 2) / 3) touchDir = 1;
|
|
402
|
+
else tryJump();
|
|
403
|
+
});
|
|
404
|
+
k.onMouseRelease(function () {
|
|
405
|
+
touchDir = 0;
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// The hazard walks back and forth near its home spot.
|
|
409
|
+
k.onUpdate("hazard", function (hazard) {
|
|
410
|
+
if (hazard.homeX === undefined) {
|
|
411
|
+
hazard.homeX = hazard.pos.x;
|
|
412
|
+
hazard.dir = 1;
|
|
413
|
+
}
|
|
414
|
+
hazard.move(hazard.dir * 70, 0);
|
|
415
|
+
if (hazard.pos.x > hazard.homeX + TILE * 2) hazard.dir = -1;
|
|
416
|
+
if (hazard.pos.x < hazard.homeX - TILE * 2) hazard.dir = 1;
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// --- Touching things ---
|
|
420
|
+
player.onCollide("coin", function (coin) {
|
|
421
|
+
k.destroy(coin);
|
|
422
|
+
score = score + 10;
|
|
423
|
+
scoreText.text = THEME.strings.scoreLabel + ": " + score;
|
|
424
|
+
});
|
|
425
|
+
player.onCollide("hazard", function () {
|
|
426
|
+
k.go("lose", { level: data.level, scoreAtStart: data.score, scoreNow: score });
|
|
427
|
+
});
|
|
428
|
+
player.onCollide("flag", function () {
|
|
429
|
+
if (data.level + 1 < LEVELS.length) {
|
|
430
|
+
k.go("game", { level: data.level + 1, score: score + 25 });
|
|
431
|
+
} else {
|
|
432
|
+
k.go("win", { score: score + 25 });
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Camera follows you. Falling off the world means a retry.
|
|
437
|
+
k.onUpdate(function () {
|
|
438
|
+
if (touchDir !== 0) player.move(touchDir * MOVE_SPEED, 0);
|
|
439
|
+
const camX = Math.min(Math.max(player.pos.x, k.width() / 2), levelWidth - k.width() / 2);
|
|
440
|
+
k.setCamPos(k.vec2(camX, k.height() / 2));
|
|
441
|
+
if (player.pos.y > k.height() + 200) {
|
|
442
|
+
k.go("lose", { level: data.level, scoreAtStart: data.score, scoreNow: score });
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// --- The retry screen ---
|
|
448
|
+
k.scene("lose", function (data) {
|
|
449
|
+
k.add([
|
|
450
|
+
k.text(THEME.strings.loseTitle, { size: 40 }),
|
|
451
|
+
k.pos(k.width() / 2, 140),
|
|
452
|
+
k.anchor("center"),
|
|
453
|
+
k.color(accent),
|
|
454
|
+
]);
|
|
455
|
+
k.add([
|
|
456
|
+
k.text(THEME.strings.loseLine, { size: 24, width: 640, align: "center" }),
|
|
457
|
+
k.pos(k.width() / 2, 240),
|
|
458
|
+
k.anchor("center"),
|
|
459
|
+
k.color(fgColor),
|
|
460
|
+
]);
|
|
461
|
+
k.add([
|
|
462
|
+
k.text(THEME.strings.scoreLabel + ": " + data.scoreNow, { size: 24 }),
|
|
463
|
+
k.pos(k.width() / 2, 330),
|
|
464
|
+
k.anchor("center"),
|
|
465
|
+
k.color(fgColor),
|
|
466
|
+
]);
|
|
467
|
+
k.add([
|
|
468
|
+
k.text(THEME.strings.retryHint, { size: 20 }),
|
|
469
|
+
k.pos(k.width() / 2, 410),
|
|
470
|
+
k.anchor("center"),
|
|
471
|
+
k.color(accent),
|
|
472
|
+
]);
|
|
473
|
+
function retry() {
|
|
474
|
+
k.go("game", { level: data.level, score: data.scoreAtStart });
|
|
475
|
+
}
|
|
476
|
+
k.onKeyPress("r", retry);
|
|
477
|
+
k.onKeyPress("space", retry);
|
|
478
|
+
k.onKeyPress("enter", retry);
|
|
479
|
+
k.onMousePress(retry);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// --- The win screen ---
|
|
483
|
+
k.scene("win", function (data) {
|
|
484
|
+
k.add([
|
|
485
|
+
k.text(THEME.strings.winTitle, { size: 44 }),
|
|
486
|
+
k.pos(k.width() / 2, 140),
|
|
487
|
+
k.anchor("center"),
|
|
488
|
+
k.color(accent),
|
|
489
|
+
]);
|
|
490
|
+
k.add([
|
|
491
|
+
k.text(THEME.strings.winLine, { size: 24, width: 640, align: "center" }),
|
|
492
|
+
k.pos(k.width() / 2, 240),
|
|
493
|
+
k.anchor("center"),
|
|
494
|
+
k.color(fgColor),
|
|
495
|
+
]);
|
|
496
|
+
k.add([
|
|
497
|
+
k.text(THEME.strings.scoreLabel + ": " + data.score, { size: 24 }),
|
|
498
|
+
k.pos(k.width() / 2, 330),
|
|
499
|
+
k.anchor("center"),
|
|
500
|
+
k.color(fgColor),
|
|
501
|
+
]);
|
|
502
|
+
k.add([
|
|
503
|
+
k.text(THEME.strings.retryHint, { size: 20 }),
|
|
504
|
+
k.pos(k.width() / 2, 410),
|
|
505
|
+
k.anchor("center"),
|
|
506
|
+
k.color(accent),
|
|
507
|
+
]);
|
|
508
|
+
function playAgain() {
|
|
509
|
+
k.go("game", { level: 0, score: 0 });
|
|
510
|
+
}
|
|
511
|
+
k.onKeyPress("r", playAgain);
|
|
512
|
+
k.onKeyPress("space", playAgain);
|
|
513
|
+
k.onKeyPress("enter", playAgain);
|
|
514
|
+
k.onMousePress(playAgain);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
k.go("start");
|
|
518
|
+
`;
|
|
519
|
+
}
|
|
520
|
+
let cachedEngine = null;
|
|
521
|
+
/** Reads the vendored engine once. Works from src (tests) and dist (build). */
|
|
522
|
+
function engineSource() {
|
|
523
|
+
if (cachedEngine === null) {
|
|
524
|
+
cachedEngine = readFileSync(new URL('./vendor/kaplay.mjs', import.meta.url), 'utf8');
|
|
525
|
+
}
|
|
526
|
+
return cachedEngine;
|
|
527
|
+
}
|
|
528
|
+
export const bigGamesScaffold = {
|
|
529
|
+
id: 'biggames',
|
|
530
|
+
label: 'Big Games',
|
|
531
|
+
emoji: 'đšī¸',
|
|
532
|
+
ageNote: 'A bigger platform game with two levels. For ages 9 and up.',
|
|
533
|
+
themes,
|
|
534
|
+
files(theme, prettyName) {
|
|
535
|
+
return {
|
|
536
|
+
'index.html': indexHtml(theme, prettyName),
|
|
537
|
+
'style.css': styleCss(theme, prettyName),
|
|
538
|
+
'game.js': gameJs(theme, prettyName),
|
|
539
|
+
};
|
|
540
|
+
},
|
|
541
|
+
starterPrompts(theme) {
|
|
542
|
+
return [
|
|
543
|
+
'make the player jump higher',
|
|
544
|
+
'add a third level with lava',
|
|
545
|
+
'make the ' + str(theme, 'hazardWord') + ' move faster',
|
|
546
|
+
'give me a double jump',
|
|
547
|
+
'make the ' + str(theme, 'collectWord') + ' worth 25 points',
|
|
548
|
+
];
|
|
549
|
+
},
|
|
550
|
+
get vendorFiles() {
|
|
551
|
+
return { 'kaplay.mjs': engineSource() };
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
export default bigGamesScaffold;
|