@hypersocial/cli-games 0.1.0 → 0.2.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/README.md +40 -15
- package/dist/cli.js +215 -164
- package/dist/cli.js.map +1 -1
- package/dist/create-NVLKKJX6.js +416 -0
- package/dist/create-NVLKKJX6.js.map +1 -0
- package/dist/index.d.ts +111 -1
- package/dist/index.js +314 -164
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/create.ts
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from "fs";
|
|
5
|
+
import { resolve, relative } from "path";
|
|
6
|
+
import { execSync, spawn } from "child_process";
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
8
|
+
function toKebab(name) {
|
|
9
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
10
|
+
}
|
|
11
|
+
function toPascal(kebab) {
|
|
12
|
+
return kebab.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
13
|
+
}
|
|
14
|
+
function toTitle(kebab) {
|
|
15
|
+
return kebab.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
|
|
16
|
+
}
|
|
17
|
+
function isCliGamesRepo(dir) {
|
|
18
|
+
return existsSync(resolve(dir, ".git")) && existsSync(resolve(dir, "src/games")) && existsSync(resolve(dir, "src/games/index.ts"));
|
|
19
|
+
}
|
|
20
|
+
function findRepoRoot(from) {
|
|
21
|
+
let dir = resolve(from);
|
|
22
|
+
const root = resolve("/");
|
|
23
|
+
while (dir !== root) {
|
|
24
|
+
if (isCliGamesRepo(dir)) return dir;
|
|
25
|
+
dir = resolve(dir, "..");
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function parseRegisteredGames(indexContent) {
|
|
30
|
+
const games = [];
|
|
31
|
+
const regex = /\{\s*id:\s*'([^']+)',\s*name:\s*'([^']+)',\s*description:\s*'([^']+)'/g;
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = regex.exec(indexContent)) !== null) {
|
|
34
|
+
games.push({ id: match[1], name: match[2], description: match[3] });
|
|
35
|
+
}
|
|
36
|
+
return games;
|
|
37
|
+
}
|
|
38
|
+
function getGames(repoRoot) {
|
|
39
|
+
const indexPath = resolve(repoRoot, "src/games/index.ts");
|
|
40
|
+
return parseRegisteredGames(readFileSync(indexPath, "utf-8"));
|
|
41
|
+
}
|
|
42
|
+
function getUserGames(repoRoot) {
|
|
43
|
+
const allGames = getGames(repoRoot);
|
|
44
|
+
let trackedFiles;
|
|
45
|
+
try {
|
|
46
|
+
trackedFiles = execSync("git ls-files src/games/", { cwd: repoRoot, encoding: "utf-8" });
|
|
47
|
+
} catch {
|
|
48
|
+
return allGames;
|
|
49
|
+
}
|
|
50
|
+
const trackedDirs = /* @__PURE__ */ new Set();
|
|
51
|
+
for (const line of trackedFiles.split("\n")) {
|
|
52
|
+
const match = line.match(/^src\/games\/([^/]+)\//);
|
|
53
|
+
if (match) trackedDirs.add(match[1]);
|
|
54
|
+
}
|
|
55
|
+
return allGames.filter((g) => !trackedDirs.has(g.id));
|
|
56
|
+
}
|
|
57
|
+
async function findOrSetupRepo() {
|
|
58
|
+
const cwd = process.cwd();
|
|
59
|
+
const found = findRepoRoot(cwd);
|
|
60
|
+
if (found) return found;
|
|
61
|
+
const setup = await p.select({
|
|
62
|
+
message: "You're not inside the cli-games repository. How would you like to set up?",
|
|
63
|
+
options: [
|
|
64
|
+
{ value: "clone", label: "Clone cli-games here", hint: `\u2192 ${cwd}/cli-games` },
|
|
65
|
+
{ value: "path", label: "I already have it cloned", hint: "enter path" },
|
|
66
|
+
{ value: "cancel", label: "Cancel" }
|
|
67
|
+
]
|
|
68
|
+
});
|
|
69
|
+
if (p.isCancel(setup) || setup === "cancel") return null;
|
|
70
|
+
if (setup === "path") {
|
|
71
|
+
const inputPath = await p.text({
|
|
72
|
+
message: "Path to your cli-games clone:",
|
|
73
|
+
validate: (value) => {
|
|
74
|
+
if (!value) return "Path is required";
|
|
75
|
+
if (!isCliGamesRepo(resolve(value))) {
|
|
76
|
+
return "Not a cli-games repo (missing src/games/index.ts or .git)";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
if (p.isCancel(inputPath)) return null;
|
|
81
|
+
return resolve(inputPath);
|
|
82
|
+
}
|
|
83
|
+
const targetDir = resolve(cwd, "cli-games");
|
|
84
|
+
if (existsSync(targetDir)) {
|
|
85
|
+
if (isCliGamesRepo(targetDir)) {
|
|
86
|
+
p.log.info("cli-games/ already exists here, using it.");
|
|
87
|
+
return targetDir;
|
|
88
|
+
}
|
|
89
|
+
p.log.error(`${targetDir} exists but doesn't look like cli-games.`);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const s = p.spinner();
|
|
93
|
+
s.start("Cloning hypersocialinc/cli-games...");
|
|
94
|
+
try {
|
|
95
|
+
execSync("git clone https://github.com/hypersocialinc/cli-games.git", {
|
|
96
|
+
cwd,
|
|
97
|
+
stdio: "ignore"
|
|
98
|
+
});
|
|
99
|
+
s.stop("Cloned cli-games.");
|
|
100
|
+
} catch {
|
|
101
|
+
s.stop("Clone failed.");
|
|
102
|
+
p.log.error("Check your internet connection and git setup.");
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
s.start("Installing dependencies...");
|
|
106
|
+
try {
|
|
107
|
+
execSync("npm install", { cwd: targetDir, stdio: "ignore" });
|
|
108
|
+
s.stop("Dependencies installed.");
|
|
109
|
+
} catch {
|
|
110
|
+
s.stop("npm install failed.");
|
|
111
|
+
p.log.warn("You may need to run npm install manually.");
|
|
112
|
+
}
|
|
113
|
+
return targetDir;
|
|
114
|
+
}
|
|
115
|
+
function addToIndex(indexPath, kebab, title, description, runFn) {
|
|
116
|
+
let index = readFileSync(indexPath, "utf-8");
|
|
117
|
+
const importRegex = /^import \{ run\w+ \} from '.\/[^']+';$/gm;
|
|
118
|
+
let lastImport = null;
|
|
119
|
+
let m;
|
|
120
|
+
while ((m = importRegex.exec(index)) !== null) lastImport = m;
|
|
121
|
+
if (lastImport) {
|
|
122
|
+
const pos = lastImport.index + lastImport[0].length;
|
|
123
|
+
index = index.slice(0, pos) + `
|
|
124
|
+
import { ${runFn} } from './${kebab}';` + index.slice(pos);
|
|
125
|
+
}
|
|
126
|
+
const entryRegex = /run: run\w+ \},?\s*$/gm;
|
|
127
|
+
let lastEntry = null;
|
|
128
|
+
while ((m = entryRegex.exec(index)) !== null) lastEntry = m;
|
|
129
|
+
if (lastEntry) {
|
|
130
|
+
const lineEnd = index.indexOf("\n", lastEntry.index);
|
|
131
|
+
const entry = `
|
|
132
|
+
{ id: '${kebab}', name: '${title}', description: '${description}', run: ${runFn} },`;
|
|
133
|
+
index = index.slice(0, lineEnd) + entry + index.slice(lineEnd);
|
|
134
|
+
}
|
|
135
|
+
const exportRegex = /^\s+run\w+(?:Game|Test),?$/gm;
|
|
136
|
+
let lastExport = null;
|
|
137
|
+
while ((m = exportRegex.exec(index)) !== null) lastExport = m;
|
|
138
|
+
if (lastExport) {
|
|
139
|
+
const lineEnd = index.indexOf("\n", lastExport.index);
|
|
140
|
+
index = index.slice(0, lineEnd) + `
|
|
141
|
+
${runFn},` + index.slice(lineEnd);
|
|
142
|
+
}
|
|
143
|
+
writeFileSync(indexPath, index);
|
|
144
|
+
}
|
|
145
|
+
function removeFromIndex(indexPath, kebab) {
|
|
146
|
+
let index = readFileSync(indexPath, "utf-8");
|
|
147
|
+
index = index.replace(new RegExp(`^import \\{ run\\w+ \\} from '\\.\\/${kebab}';\\n`, "gm"), "");
|
|
148
|
+
index = index.replace(new RegExp(`^\\s*\\{[^}]*id:\\s*'${kebab}'[^}]*\\},?\\n`, "gm"), "");
|
|
149
|
+
const pascal = toPascal(kebab);
|
|
150
|
+
const runFn = `run${pascal}Game`;
|
|
151
|
+
index = index.replace(new RegExp(`^\\s+${runFn},?\\n`, "gm"), "");
|
|
152
|
+
writeFileSync(indexPath, index);
|
|
153
|
+
}
|
|
154
|
+
function launchClaude(repoRoot, prompt) {
|
|
155
|
+
p.log.info("Launching Claude Code...");
|
|
156
|
+
const child = spawn("claude", [prompt], {
|
|
157
|
+
cwd: repoRoot,
|
|
158
|
+
stdio: "inherit"
|
|
159
|
+
});
|
|
160
|
+
return new Promise((res) => {
|
|
161
|
+
child.on("close", () => res());
|
|
162
|
+
child.on("error", () => {
|
|
163
|
+
p.log.error("Could not launch Claude Code. Is it installed? Run: npm install -g @anthropic-ai/claude-code");
|
|
164
|
+
res();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async function doCreate(repoRoot, initialName) {
|
|
169
|
+
let kebab;
|
|
170
|
+
if (initialName) {
|
|
171
|
+
kebab = toKebab(initialName);
|
|
172
|
+
} else {
|
|
173
|
+
const nameInput = await p.text({
|
|
174
|
+
message: "What's your game called?",
|
|
175
|
+
placeholder: "space-dodge",
|
|
176
|
+
validate: (value) => {
|
|
177
|
+
if (!value) return "Name is required";
|
|
178
|
+
const k = toKebab(value);
|
|
179
|
+
if (!/^[a-z][a-z0-9-]*$/.test(k) || k.length < 2) {
|
|
180
|
+
return "Use lowercase letters, numbers, and hyphens (e.g. space-dodge)";
|
|
181
|
+
}
|
|
182
|
+
if (existsSync(resolve(repoRoot, "src/games", k))) {
|
|
183
|
+
return `Game "${k}" already exists`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
if (p.isCancel(nameInput)) return;
|
|
188
|
+
kebab = toKebab(nameInput);
|
|
189
|
+
}
|
|
190
|
+
if (!/^[a-z][a-z0-9-]*$/.test(kebab) || kebab.length < 2) {
|
|
191
|
+
p.log.error(`Invalid game name: "${kebab}"`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const gamesDir = resolve(repoRoot, "src/games");
|
|
195
|
+
const gameDir = resolve(gamesDir, kebab);
|
|
196
|
+
if (existsSync(gameDir)) {
|
|
197
|
+
p.log.error(`Game "${kebab}" already exists at src/games/${kebab}/`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const descInput = await p.text({
|
|
201
|
+
message: "Describe your game in a few words:",
|
|
202
|
+
placeholder: "A terminal game",
|
|
203
|
+
defaultValue: "A terminal game"
|
|
204
|
+
});
|
|
205
|
+
if (p.isCancel(descInput)) return;
|
|
206
|
+
const description = descInput || "A terminal game";
|
|
207
|
+
const pascal = toPascal(kebab);
|
|
208
|
+
const title = toTitle(kebab);
|
|
209
|
+
const runFn = `run${pascal}Game`;
|
|
210
|
+
const skillPath = resolve(repoRoot, ".claude/skills/game-dev");
|
|
211
|
+
if (!existsSync(skillPath)) {
|
|
212
|
+
const s = p.spinner();
|
|
213
|
+
s.start("Installing game-dev skill for Claude Code...");
|
|
214
|
+
try {
|
|
215
|
+
execSync("npx skills add hypersocialinc/cli-games -a claude-code -s game-dev -y", {
|
|
216
|
+
cwd: repoRoot,
|
|
217
|
+
stdio: "ignore"
|
|
218
|
+
});
|
|
219
|
+
s.stop("Installed game-dev skill.");
|
|
220
|
+
} catch {
|
|
221
|
+
s.stop("Could not install game-dev skill.");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const templatePath = resolve(repoRoot, ".claude/skills/game-dev/templates/game-scaffold.ts");
|
|
225
|
+
if (!existsSync(templatePath)) {
|
|
226
|
+
p.cancel(`Template not found at ${templatePath}`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
let template = readFileSync(templatePath, "utf-8");
|
|
230
|
+
template = template.replace(/\{GameName\}/g, pascal);
|
|
231
|
+
template = template.replace(/\{GAME_NAME\}/g, title);
|
|
232
|
+
template = template.replace(/\{GAME_DESCRIPTION\}/g, description);
|
|
233
|
+
template = template.replace(/\{TITLE_LINE_1\}/g, title.toUpperCase());
|
|
234
|
+
template = template.replace(/\{TITLE_LINE_2\}/g, "\u2550".repeat(title.length));
|
|
235
|
+
template = template.replace(/\{CONTROLS_HINT\}/g, "Arrow keys to move, Space to act");
|
|
236
|
+
mkdirSync(gameDir, { recursive: true });
|
|
237
|
+
writeFileSync(resolve(gameDir, "index.ts"), template);
|
|
238
|
+
p.log.success(`Created src/games/${kebab}/index.ts`);
|
|
239
|
+
addToIndex(resolve(gamesDir, "index.ts"), kebab, title, description, runFn);
|
|
240
|
+
p.log.success("Registered in src/games/index.ts");
|
|
241
|
+
const vibeNow = await p.confirm({
|
|
242
|
+
message: "Launch Claude Code to start vibe coding?"
|
|
243
|
+
});
|
|
244
|
+
if (!p.isCancel(vibeNow) && vibeNow) {
|
|
245
|
+
await launchClaude(repoRoot, `Build out the ${kebab} game. It should be: ${description}. Use the game-dev skill \u2014 check src/games/${kebab}/index.ts for the scaffold.`);
|
|
246
|
+
} else {
|
|
247
|
+
const needsCd = resolve(repoRoot) !== resolve(process.cwd());
|
|
248
|
+
const rel = relative(process.cwd(), repoRoot);
|
|
249
|
+
const cdPath = rel.startsWith("..") ? resolve(repoRoot) : rel;
|
|
250
|
+
const steps = [
|
|
251
|
+
...needsCd ? [`cd ${cdPath}`] : [],
|
|
252
|
+
"Open Claude Code in this directory",
|
|
253
|
+
`Tell Claude: "Build out the ${kebab} game \u2014 make it a [your idea]"`,
|
|
254
|
+
`Test with: npx cli-games ${kebab}`,
|
|
255
|
+
"Submit a PR when ready!"
|
|
256
|
+
];
|
|
257
|
+
p.note(steps.map((s, i) => `${i + 1}. ${s}`).join("\n"), "Next steps");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function playGame(repoRoot, gameId) {
|
|
261
|
+
const s = p.spinner();
|
|
262
|
+
s.start("Building...");
|
|
263
|
+
try {
|
|
264
|
+
execSync("npm run build", { cwd: repoRoot, stdio: "ignore" });
|
|
265
|
+
s.stop("Build complete.");
|
|
266
|
+
} catch {
|
|
267
|
+
s.stop("Build failed.");
|
|
268
|
+
p.log.error("Fix build errors and try again.");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
p.log.info(`Launching ${gameId}... (press Q to quit back here)`);
|
|
272
|
+
const cliPath = resolve(repoRoot, "dist/cli.js");
|
|
273
|
+
const child = spawn("node", [cliPath, gameId], {
|
|
274
|
+
cwd: repoRoot,
|
|
275
|
+
stdio: "inherit"
|
|
276
|
+
});
|
|
277
|
+
await new Promise((res) => {
|
|
278
|
+
child.on("close", () => res());
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
async function vibeCodeGame(repoRoot, gameId) {
|
|
282
|
+
const idea = await p.text({
|
|
283
|
+
message: "What do you want Claude to do?",
|
|
284
|
+
placeholder: `Make ${gameId} a fast-paced bullet-hell dodger`
|
|
285
|
+
});
|
|
286
|
+
if (p.isCancel(idea) || !idea) return;
|
|
287
|
+
await launchClaude(repoRoot, `${idea}. Work on src/games/${gameId}/index.ts. Use the game-dev skill for patterns and conventions.`);
|
|
288
|
+
}
|
|
289
|
+
async function removeGame(repoRoot, gameId) {
|
|
290
|
+
const gamesDir = resolve(repoRoot, "src/games");
|
|
291
|
+
const game = getGames(repoRoot).find((g) => g.id === gameId);
|
|
292
|
+
const gameDir = resolve(gamesDir, gameId);
|
|
293
|
+
const confirmed = await p.confirm({
|
|
294
|
+
message: `Remove ${game.name}? This deletes src/games/${gameId}/ and unregisters it.`
|
|
295
|
+
});
|
|
296
|
+
if (p.isCancel(confirmed) || !confirmed) return;
|
|
297
|
+
if (existsSync(gameDir)) {
|
|
298
|
+
rmSync(gameDir, { recursive: true });
|
|
299
|
+
p.log.success(`Deleted src/games/${gameId}/`);
|
|
300
|
+
}
|
|
301
|
+
removeFromIndex(resolve(gamesDir, "index.ts"), gameId);
|
|
302
|
+
p.log.success("Unregistered from src/games/index.ts");
|
|
303
|
+
}
|
|
304
|
+
async function submitPR(repoRoot, gameId) {
|
|
305
|
+
const game = getGames(repoRoot).find((g) => g.id === gameId);
|
|
306
|
+
await launchClaude(repoRoot, `Help me submit a PR for the ${game.name} game. Check git status, create a branch if needed, commit the changes in src/games/${gameId}/ and src/games/index.ts, and open a PR.`);
|
|
307
|
+
}
|
|
308
|
+
async function showGameActions(repoRoot, gameId) {
|
|
309
|
+
const game = getGames(repoRoot).find((g) => g.id === gameId);
|
|
310
|
+
const action = await p.select({
|
|
311
|
+
message: `${game.name} \u2014 ${game.description}`,
|
|
312
|
+
options: [
|
|
313
|
+
{ value: "play", label: "Play", hint: "build & launch" },
|
|
314
|
+
{ value: "vibe", label: "Vibe code", hint: "launch Claude Code" },
|
|
315
|
+
{ value: "pr", label: "Submit a PR", hint: "launch Claude Code" },
|
|
316
|
+
{ value: "remove", label: "Remove", hint: "delete + unregister" },
|
|
317
|
+
{ value: "back", label: "Back" }
|
|
318
|
+
]
|
|
319
|
+
});
|
|
320
|
+
if (p.isCancel(action) || action === "back") return;
|
|
321
|
+
if (action === "play") await playGame(repoRoot, gameId);
|
|
322
|
+
if (action === "vibe") await vibeCodeGame(repoRoot, gameId);
|
|
323
|
+
if (action === "pr") await submitPR(repoRoot, gameId);
|
|
324
|
+
if (action === "remove") await removeGame(repoRoot, gameId);
|
|
325
|
+
}
|
|
326
|
+
async function showDevMenu(repoRoot) {
|
|
327
|
+
const userGames = getUserGames(repoRoot);
|
|
328
|
+
const options = [
|
|
329
|
+
{ value: "create", label: "Create a new game" }
|
|
330
|
+
];
|
|
331
|
+
if (userGames.length > 0) {
|
|
332
|
+
options.push({ value: "games", label: "Your games", hint: `${userGames.length} game${userGames.length === 1 ? "" : "s"}` });
|
|
333
|
+
}
|
|
334
|
+
options.push({ value: "exit", label: "Exit" });
|
|
335
|
+
const action = await p.select({ message: "What would you like to do?", options });
|
|
336
|
+
if (p.isCancel(action) || action === "exit") return;
|
|
337
|
+
if (action === "create") {
|
|
338
|
+
await doCreate(repoRoot);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (action === "games") {
|
|
342
|
+
const selected = await p.select({
|
|
343
|
+
message: "Pick a game:",
|
|
344
|
+
options: userGames.map((g) => ({
|
|
345
|
+
value: g.id,
|
|
346
|
+
label: g.name,
|
|
347
|
+
hint: g.description
|
|
348
|
+
}))
|
|
349
|
+
});
|
|
350
|
+
if (p.isCancel(selected)) return;
|
|
351
|
+
await showGameActions(repoRoot, selected);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async function vibeCommand(args) {
|
|
355
|
+
const name = args.filter((a) => !a.startsWith("--"))[0];
|
|
356
|
+
p.intro("cli-games");
|
|
357
|
+
const repoRoot = await findOrSetupRepo();
|
|
358
|
+
if (!repoRoot) {
|
|
359
|
+
p.cancel("Cancelled.");
|
|
360
|
+
process.exit(0);
|
|
361
|
+
}
|
|
362
|
+
if (name) {
|
|
363
|
+
const kebab = toKebab(name);
|
|
364
|
+
const games = getGames(repoRoot);
|
|
365
|
+
const existing = games.find((g) => g.id === kebab);
|
|
366
|
+
if (existing) {
|
|
367
|
+
await showGameActions(repoRoot, kebab);
|
|
368
|
+
} else {
|
|
369
|
+
await doCreate(repoRoot, name);
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
await showDevMenu(repoRoot);
|
|
373
|
+
}
|
|
374
|
+
p.outro("Happy building!");
|
|
375
|
+
}
|
|
376
|
+
async function removeCommand(args) {
|
|
377
|
+
const name = args.filter((a) => !a.startsWith("--"))[0];
|
|
378
|
+
p.intro("cli-games");
|
|
379
|
+
const repoRoot = await findOrSetupRepo();
|
|
380
|
+
if (!repoRoot) {
|
|
381
|
+
p.cancel("Cancelled.");
|
|
382
|
+
process.exit(0);
|
|
383
|
+
}
|
|
384
|
+
if (name) {
|
|
385
|
+
const kebab = toKebab(name);
|
|
386
|
+
const games = getGames(repoRoot);
|
|
387
|
+
if (!games.find((g) => g.id === kebab)) {
|
|
388
|
+
p.log.error(`Game "${kebab}" not found.`);
|
|
389
|
+
} else {
|
|
390
|
+
await removeGame(repoRoot, kebab);
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
const games = getGames(repoRoot);
|
|
394
|
+
if (games.length === 0) {
|
|
395
|
+
p.log.warn("No games found.");
|
|
396
|
+
} else {
|
|
397
|
+
const selected = await p.select({
|
|
398
|
+
message: "Which game do you want to remove?",
|
|
399
|
+
options: games.map((g) => ({
|
|
400
|
+
value: g.id,
|
|
401
|
+
label: g.name,
|
|
402
|
+
hint: g.description
|
|
403
|
+
}))
|
|
404
|
+
});
|
|
405
|
+
if (!p.isCancel(selected)) {
|
|
406
|
+
await removeGame(repoRoot, selected);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
p.outro("Done.");
|
|
411
|
+
}
|
|
412
|
+
export {
|
|
413
|
+
removeCommand,
|
|
414
|
+
vibeCommand
|
|
415
|
+
};
|
|
416
|
+
//# sourceMappingURL=create-NVLKKJX6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/create.ts"],"sourcesContent":["/**\n * `cli-games vibe` — developer hub for cli-games\n *\n * Interactive TUI for creating, vibe coding, playing, removing games,\n * and submitting PRs — all powered by clack prompts and Claude Code.\n */\n\nimport { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'fs';\nimport { resolve, relative } from 'path';\nimport { execSync, spawn } from 'child_process';\nimport * as p from '@clack/prompts';\n\n// ---------------------------------------------------------------------------\n// Name utilities\n// ---------------------------------------------------------------------------\n\nfunction toKebab(name: string): string {\n return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');\n}\n\nfunction toPascal(kebab: string): string {\n return kebab.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');\n}\n\nfunction toTitle(kebab: string): string {\n return kebab.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ');\n}\n\n// ---------------------------------------------------------------------------\n// Repo detection\n// ---------------------------------------------------------------------------\n\nfunction isCliGamesRepo(dir: string): boolean {\n return existsSync(resolve(dir, '.git')) &&\n existsSync(resolve(dir, 'src/games')) &&\n existsSync(resolve(dir, 'src/games/index.ts'));\n}\n\nfunction findRepoRoot(from: string): string | null {\n let dir = resolve(from);\n const root = resolve('/');\n while (dir !== root) {\n if (isCliGamesRepo(dir)) return dir;\n dir = resolve(dir, '..');\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Game registry parsing\n// ---------------------------------------------------------------------------\n\ninterface RegisteredGame {\n id: string;\n name: string;\n description: string;\n}\n\nfunction parseRegisteredGames(indexContent: string): RegisteredGame[] {\n const games: RegisteredGame[] = [];\n const regex = /\\{\\s*id:\\s*'([^']+)',\\s*name:\\s*'([^']+)',\\s*description:\\s*'([^']+)'/g;\n let match;\n while ((match = regex.exec(indexContent)) !== null) {\n games.push({ id: match[1], name: match[2], description: match[3] });\n }\n return games;\n}\n\nfunction getGames(repoRoot: string): RegisteredGame[] {\n const indexPath = resolve(repoRoot, 'src/games/index.ts');\n return parseRegisteredGames(readFileSync(indexPath, 'utf-8'));\n}\n\nfunction getUserGames(repoRoot: string): RegisteredGame[] {\n const allGames = getGames(repoRoot);\n\n // Games tracked in git are built-in; untracked ones are the user's\n let trackedFiles: string;\n try {\n trackedFiles = execSync('git ls-files src/games/', { cwd: repoRoot, encoding: 'utf-8' });\n } catch {\n return allGames;\n }\n\n const trackedDirs = new Set<string>();\n for (const line of trackedFiles.split('\\n')) {\n const match = line.match(/^src\\/games\\/([^/]+)\\//);\n if (match) trackedDirs.add(match[1]);\n }\n\n return allGames.filter(g => !trackedDirs.has(g.id));\n}\n\n// ---------------------------------------------------------------------------\n// Interactive repo setup\n// ---------------------------------------------------------------------------\n\nasync function findOrSetupRepo(): Promise<string | null> {\n const cwd = process.cwd();\n const found = findRepoRoot(cwd);\n if (found) return found;\n\n const setup = await p.select({\n message: 'You\\'re not inside the cli-games repository. How would you like to set up?',\n options: [\n { value: 'clone', label: 'Clone cli-games here', hint: `→ ${cwd}/cli-games` },\n { value: 'path', label: 'I already have it cloned', hint: 'enter path' },\n { value: 'cancel', label: 'Cancel' },\n ],\n });\n\n if (p.isCancel(setup) || setup === 'cancel') return null;\n\n if (setup === 'path') {\n const inputPath = await p.text({\n message: 'Path to your cli-games clone:',\n validate: (value) => {\n if (!value) return 'Path is required';\n if (!isCliGamesRepo(resolve(value))) {\n return 'Not a cli-games repo (missing src/games/index.ts or .git)';\n }\n },\n });\n if (p.isCancel(inputPath)) return null;\n return resolve(inputPath);\n }\n\n const targetDir = resolve(cwd, 'cli-games');\n if (existsSync(targetDir)) {\n if (isCliGamesRepo(targetDir)) {\n p.log.info('cli-games/ already exists here, using it.');\n return targetDir;\n }\n p.log.error(`${targetDir} exists but doesn't look like cli-games.`);\n return null;\n }\n\n const s = p.spinner();\n s.start('Cloning hypersocialinc/cli-games...');\n try {\n execSync('git clone https://github.com/hypersocialinc/cli-games.git', {\n cwd,\n stdio: 'ignore',\n });\n s.stop('Cloned cli-games.');\n } catch {\n s.stop('Clone failed.');\n p.log.error('Check your internet connection and git setup.');\n return null;\n }\n\n s.start('Installing dependencies...');\n try {\n execSync('npm install', { cwd: targetDir, stdio: 'ignore' });\n s.stop('Dependencies installed.');\n } catch {\n s.stop('npm install failed.');\n p.log.warn('You may need to run npm install manually.');\n }\n\n return targetDir;\n}\n\n// ---------------------------------------------------------------------------\n// Index manipulation\n// ---------------------------------------------------------------------------\n\nfunction addToIndex(indexPath: string, kebab: string, title: string, description: string, runFn: string) {\n let index = readFileSync(indexPath, 'utf-8');\n\n const importRegex = /^import \\{ run\\w+ \\} from '.\\/[^']+';$/gm;\n let lastImport: RegExpExecArray | null = null;\n let m: RegExpExecArray | null;\n while ((m = importRegex.exec(index)) !== null) lastImport = m;\n\n if (lastImport) {\n const pos = lastImport.index + lastImport[0].length;\n index = index.slice(0, pos) + `\\nimport { ${runFn} } from './${kebab}';` + index.slice(pos);\n }\n\n const entryRegex = /run: run\\w+ \\},?\\s*$/gm;\n let lastEntry: RegExpExecArray | null = null;\n while ((m = entryRegex.exec(index)) !== null) lastEntry = m;\n\n if (lastEntry) {\n const lineEnd = index.indexOf('\\n', lastEntry.index);\n const entry = `\\n { id: '${kebab}', name: '${title}', description: '${description}', run: ${runFn} },`;\n index = index.slice(0, lineEnd) + entry + index.slice(lineEnd);\n }\n\n const exportRegex = /^\\s+run\\w+(?:Game|Test),?$/gm;\n let lastExport: RegExpExecArray | null = null;\n while ((m = exportRegex.exec(index)) !== null) lastExport = m;\n\n if (lastExport) {\n const lineEnd = index.indexOf('\\n', lastExport.index);\n index = index.slice(0, lineEnd) + `\\n ${runFn},` + index.slice(lineEnd);\n }\n\n writeFileSync(indexPath, index);\n}\n\nfunction removeFromIndex(indexPath: string, kebab: string) {\n let index = readFileSync(indexPath, 'utf-8');\n\n index = index.replace(new RegExp(`^import \\\\{ run\\\\w+ \\\\} from '\\\\.\\\\/${kebab}';\\\\n`, 'gm'), '');\n index = index.replace(new RegExp(`^\\\\s*\\\\{[^}]*id:\\\\s*'${kebab}'[^}]*\\\\},?\\\\n`, 'gm'), '');\n\n const pascal = toPascal(kebab);\n const runFn = `run${pascal}Game`;\n index = index.replace(new RegExp(`^\\\\s+${runFn},?\\\\n`, 'gm'), '');\n\n writeFileSync(indexPath, index);\n}\n\n// ---------------------------------------------------------------------------\n// Launch Claude Code\n// ---------------------------------------------------------------------------\n\nfunction launchClaude(repoRoot: string, prompt: string): Promise<void> {\n p.log.info('Launching Claude Code...');\n\n const child = spawn('claude', [prompt], {\n cwd: repoRoot,\n stdio: 'inherit',\n });\n\n return new Promise((res) => {\n child.on('close', () => res());\n child.on('error', () => {\n p.log.error('Could not launch Claude Code. Is it installed? Run: npm install -g @anthropic-ai/claude-code');\n res();\n });\n });\n}\n\n// ---------------------------------------------------------------------------\n// Actions\n// ---------------------------------------------------------------------------\n\nasync function doCreate(repoRoot: string, initialName?: string) {\n let kebab: string;\n if (initialName) {\n kebab = toKebab(initialName);\n } else {\n const nameInput = await p.text({\n message: 'What\\'s your game called?',\n placeholder: 'space-dodge',\n validate: (value) => {\n if (!value) return 'Name is required';\n const k = toKebab(value);\n if (!/^[a-z][a-z0-9-]*$/.test(k) || k.length < 2) {\n return 'Use lowercase letters, numbers, and hyphens (e.g. space-dodge)';\n }\n if (existsSync(resolve(repoRoot, 'src/games', k))) {\n return `Game \"${k}\" already exists`;\n }\n },\n });\n if (p.isCancel(nameInput)) return;\n kebab = toKebab(nameInput);\n }\n\n if (!/^[a-z][a-z0-9-]*$/.test(kebab) || kebab.length < 2) {\n p.log.error(`Invalid game name: \"${kebab}\"`);\n return;\n }\n\n const gamesDir = resolve(repoRoot, 'src/games');\n const gameDir = resolve(gamesDir, kebab);\n\n if (existsSync(gameDir)) {\n p.log.error(`Game \"${kebab}\" already exists at src/games/${kebab}/`);\n return;\n }\n\n const descInput = await p.text({\n message: 'Describe your game in a few words:',\n placeholder: 'A terminal game',\n defaultValue: 'A terminal game',\n });\n if (p.isCancel(descInput)) return;\n const description = descInput || 'A terminal game';\n\n const pascal = toPascal(kebab);\n const title = toTitle(kebab);\n const runFn = `run${pascal}Game`;\n\n // Install skill if needed\n const skillPath = resolve(repoRoot, '.claude/skills/game-dev');\n if (!existsSync(skillPath)) {\n const s = p.spinner();\n s.start('Installing game-dev skill for Claude Code...');\n try {\n execSync('npx skills add hypersocialinc/cli-games -a claude-code -s game-dev -y', {\n cwd: repoRoot,\n stdio: 'ignore',\n });\n s.stop('Installed game-dev skill.');\n } catch {\n s.stop('Could not install game-dev skill.');\n }\n }\n\n // Read and fill template\n const templatePath = resolve(repoRoot, '.claude/skills/game-dev/templates/game-scaffold.ts');\n if (!existsSync(templatePath)) {\n p.cancel(`Template not found at ${templatePath}`);\n return;\n }\n\n let template = readFileSync(templatePath, 'utf-8');\n template = template.replace(/\\{GameName\\}/g, pascal);\n template = template.replace(/\\{GAME_NAME\\}/g, title);\n template = template.replace(/\\{GAME_DESCRIPTION\\}/g, description);\n template = template.replace(/\\{TITLE_LINE_1\\}/g, title.toUpperCase());\n template = template.replace(/\\{TITLE_LINE_2\\}/g, '═'.repeat(title.length));\n template = template.replace(/\\{CONTROLS_HINT\\}/g, 'Arrow keys to move, Space to act');\n\n mkdirSync(gameDir, { recursive: true });\n writeFileSync(resolve(gameDir, 'index.ts'), template);\n p.log.success(`Created src/games/${kebab}/index.ts`);\n\n addToIndex(resolve(gamesDir, 'index.ts'), kebab, title, description, runFn);\n p.log.success('Registered in src/games/index.ts');\n\n // Offer to launch Claude Code\n const vibeNow = await p.confirm({\n message: 'Launch Claude Code to start vibe coding?',\n });\n if (!p.isCancel(vibeNow) && vibeNow) {\n await launchClaude(repoRoot, `Build out the ${kebab} game. It should be: ${description}. Use the game-dev skill — check src/games/${kebab}/index.ts for the scaffold.`);\n } else {\n const needsCd = resolve(repoRoot) !== resolve(process.cwd());\n const rel = relative(process.cwd(), repoRoot);\n const cdPath = rel.startsWith('..') ? resolve(repoRoot) : rel;\n\n const steps = [\n ...(needsCd ? [`cd ${cdPath}`] : []),\n 'Open Claude Code in this directory',\n `Tell Claude: \"Build out the ${kebab} game — make it a [your idea]\"`,\n `Test with: npx cli-games ${kebab}`,\n 'Submit a PR when ready!',\n ];\n\n p.note(steps.map((s, i) => `${i + 1}. ${s}`).join('\\n'), 'Next steps');\n }\n}\n\n// ---------------------------------------------------------------------------\n// Game actions — second-level menu after picking a game\n// ---------------------------------------------------------------------------\n\nasync function playGame(repoRoot: string, gameId: string) {\n const s = p.spinner();\n s.start('Building...');\n try {\n execSync('npm run build', { cwd: repoRoot, stdio: 'ignore' });\n s.stop('Build complete.');\n } catch {\n s.stop('Build failed.');\n p.log.error('Fix build errors and try again.');\n return;\n }\n\n p.log.info(`Launching ${gameId}... (press Q to quit back here)`);\n\n const cliPath = resolve(repoRoot, 'dist/cli.js');\n const child = spawn('node', [cliPath, gameId], {\n cwd: repoRoot,\n stdio: 'inherit',\n });\n\n await new Promise<void>((res) => {\n child.on('close', () => res());\n });\n}\n\nasync function vibeCodeGame(repoRoot: string, gameId: string) {\n const idea = await p.text({\n message: 'What do you want Claude to do?',\n placeholder: `Make ${gameId} a fast-paced bullet-hell dodger`,\n });\n if (p.isCancel(idea) || !idea) return;\n\n await launchClaude(repoRoot, `${idea}. Work on src/games/${gameId}/index.ts. Use the game-dev skill for patterns and conventions.`);\n}\n\nasync function removeGame(repoRoot: string, gameId: string) {\n const gamesDir = resolve(repoRoot, 'src/games');\n const game = getGames(repoRoot).find(g => g.id === gameId)!;\n const gameDir = resolve(gamesDir, gameId);\n\n const confirmed = await p.confirm({\n message: `Remove ${game.name}? This deletes src/games/${gameId}/ and unregisters it.`,\n });\n if (p.isCancel(confirmed) || !confirmed) return;\n\n if (existsSync(gameDir)) {\n rmSync(gameDir, { recursive: true });\n p.log.success(`Deleted src/games/${gameId}/`);\n }\n\n removeFromIndex(resolve(gamesDir, 'index.ts'), gameId);\n p.log.success('Unregistered from src/games/index.ts');\n}\n\nasync function submitPR(repoRoot: string, gameId: string) {\n const game = getGames(repoRoot).find(g => g.id === gameId)!;\n await launchClaude(repoRoot, `Help me submit a PR for the ${game.name} game. Check git status, create a branch if needed, commit the changes in src/games/${gameId}/ and src/games/index.ts, and open a PR.`);\n}\n\nasync function showGameActions(repoRoot: string, gameId: string) {\n const game = getGames(repoRoot).find(g => g.id === gameId)!;\n\n const action = await p.select({\n message: `${game.name} — ${game.description}`,\n options: [\n { value: 'play', label: 'Play', hint: 'build & launch' },\n { value: 'vibe', label: 'Vibe code', hint: 'launch Claude Code' },\n { value: 'pr', label: 'Submit a PR', hint: 'launch Claude Code' },\n { value: 'remove', label: 'Remove', hint: 'delete + unregister' },\n { value: 'back', label: 'Back' },\n ],\n });\n\n if (p.isCancel(action) || action === 'back') return;\n\n if (action === 'play') await playGame(repoRoot, gameId);\n if (action === 'vibe') await vibeCodeGame(repoRoot, gameId);\n if (action === 'pr') await submitPR(repoRoot, gameId);\n if (action === 'remove') await removeGame(repoRoot, gameId);\n}\n\n// ---------------------------------------------------------------------------\n// Dev menu — top level\n// ---------------------------------------------------------------------------\n\nasync function showDevMenu(repoRoot: string) {\n const userGames = getUserGames(repoRoot);\n\n type MenuOption = { value: string; label: string; hint?: string };\n const options: MenuOption[] = [\n { value: 'create', label: 'Create a new game' },\n ];\n\n if (userGames.length > 0) {\n options.push({ value: 'games', label: 'Your games', hint: `${userGames.length} game${userGames.length === 1 ? '' : 's'}` });\n }\n\n options.push({ value: 'exit', label: 'Exit' });\n\n const action = await p.select({ message: 'What would you like to do?', options });\n\n if (p.isCancel(action) || action === 'exit') return;\n\n if (action === 'create') {\n await doCreate(repoRoot);\n return;\n }\n\n if (action === 'games') {\n const selected = await p.select({\n message: 'Pick a game:',\n options: userGames.map(g => ({\n value: g.id,\n label: g.name,\n hint: g.description,\n })),\n });\n if (p.isCancel(selected)) return;\n\n await showGameActions(repoRoot, selected);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Entry points — called from cli.ts\n// ---------------------------------------------------------------------------\n\nexport async function vibeCommand(args: string[]) {\n const name = args.filter(a => !a.startsWith('--'))[0];\n\n p.intro('cli-games');\n\n const repoRoot = await findOrSetupRepo();\n if (!repoRoot) {\n p.cancel('Cancelled.');\n process.exit(0);\n }\n\n if (name) {\n // cli-games vibe <name> — if game exists, show actions; if not, create it\n const kebab = toKebab(name);\n const games = getGames(repoRoot);\n const existing = games.find(g => g.id === kebab);\n if (existing) {\n await showGameActions(repoRoot, kebab);\n } else {\n await doCreate(repoRoot, name);\n }\n } else {\n await showDevMenu(repoRoot);\n }\n\n p.outro('Happy building!');\n}\n\nexport async function removeCommand(args: string[]) {\n const name = args.filter(a => !a.startsWith('--'))[0];\n\n p.intro('cli-games');\n\n const repoRoot = await findOrSetupRepo();\n if (!repoRoot) {\n p.cancel('Cancelled.');\n process.exit(0);\n }\n\n if (name) {\n const kebab = toKebab(name);\n const games = getGames(repoRoot);\n if (!games.find(g => g.id === kebab)) {\n p.log.error(`Game \"${kebab}\" not found.`);\n } else {\n await removeGame(repoRoot, kebab);\n }\n } else {\n // No name — show game picker\n const games = getGames(repoRoot);\n if (games.length === 0) {\n p.log.warn('No games found.');\n } else {\n const selected = await p.select({\n message: 'Which game do you want to remove?',\n options: games.map(g => ({\n value: g.id,\n label: g.name,\n hint: g.description,\n })),\n });\n if (!p.isCancel(selected)) {\n await removeGame(repoRoot, selected);\n }\n }\n }\n\n p.outro('Done.');\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -663,6 +663,116 @@ interface RebootController {
|
|
|
663
663
|
*/
|
|
664
664
|
declare function runRebootEffect(terminal: Terminal): RebootController;
|
|
665
665
|
|
|
666
|
+
/**
|
|
667
|
+
* Shared Visual Effects
|
|
668
|
+
*
|
|
669
|
+
* Reusable particle systems, score popups, screen shake, and flash effects.
|
|
670
|
+
* Import these in new games instead of re-implementing effect logic.
|
|
671
|
+
*
|
|
672
|
+
* Based on patterns from chopper/effects.ts and used across 16+ games.
|
|
673
|
+
*/
|
|
674
|
+
interface Particle {
|
|
675
|
+
x: number;
|
|
676
|
+
y: number;
|
|
677
|
+
char: string;
|
|
678
|
+
color: string;
|
|
679
|
+
vx: number;
|
|
680
|
+
vy: number;
|
|
681
|
+
life: number;
|
|
682
|
+
}
|
|
683
|
+
interface ScorePopup {
|
|
684
|
+
x: number;
|
|
685
|
+
y: number;
|
|
686
|
+
text: string;
|
|
687
|
+
frames: number;
|
|
688
|
+
color: string;
|
|
689
|
+
}
|
|
690
|
+
interface ScreenShakeState {
|
|
691
|
+
frames: number;
|
|
692
|
+
intensity: number;
|
|
693
|
+
}
|
|
694
|
+
interface FlashState {
|
|
695
|
+
frames: number;
|
|
696
|
+
}
|
|
697
|
+
/** Maximum particles to prevent performance issues */
|
|
698
|
+
declare const MAX_PARTICLES = 100;
|
|
699
|
+
/** Common particle character sets */
|
|
700
|
+
declare const PARTICLE_CHARS: {
|
|
701
|
+
readonly explosion: readonly ["✗", "×", "·", "○", "▒", "░"];
|
|
702
|
+
readonly success: readonly ["✦", "★", "◆", "●", "♦"];
|
|
703
|
+
readonly fire: readonly ["▓", "▒", "░", "●", "◆"];
|
|
704
|
+
readonly death: readonly ["✗", "☠", "×", "▓", "░"];
|
|
705
|
+
readonly sparkle: readonly ["✦", "✧", "★"];
|
|
706
|
+
readonly firework: readonly ["★", "✦", "◆", "●", "✶", "✴", "◇", "♦", "•", "○"];
|
|
707
|
+
};
|
|
708
|
+
/** Bright ANSI colors for fireworks and celebrations */
|
|
709
|
+
declare const FIREWORK_COLORS: string[];
|
|
710
|
+
/**
|
|
711
|
+
* Spawn particles in a radial burst pattern.
|
|
712
|
+
* Respects MAX_PARTICLES to prevent performance issues.
|
|
713
|
+
*/
|
|
714
|
+
declare function spawnParticles(particles: Particle[], x: number, y: number, count: number, color: string, chars?: string[]): void;
|
|
715
|
+
/**
|
|
716
|
+
* Spawn a colorful firework explosion with central burst and sparkle ring.
|
|
717
|
+
*/
|
|
718
|
+
declare function spawnFirework(particles: Particle[], x: number, y: number, intensity?: number): void;
|
|
719
|
+
/**
|
|
720
|
+
* Spawn rising sparkle trail effect from a single point.
|
|
721
|
+
*/
|
|
722
|
+
declare function spawnSparkleTrail(particles: Particle[], x: number, y: number, count?: number, color?: string): void;
|
|
723
|
+
/**
|
|
724
|
+
* Update all particles: apply velocity, gravity, and remove dead ones.
|
|
725
|
+
*/
|
|
726
|
+
declare function updateParticles(particles: Particle[], gravityMult?: number): void;
|
|
727
|
+
/**
|
|
728
|
+
* Add a floating text popup that rises and fades.
|
|
729
|
+
*/
|
|
730
|
+
declare function addScorePopup(popups: ScorePopup[], x: number, y: number, text: string, color?: string): void;
|
|
731
|
+
/**
|
|
732
|
+
* Update all popups: float upward and remove expired ones.
|
|
733
|
+
*/
|
|
734
|
+
declare function updatePopups(popups: ScorePopup[]): void;
|
|
735
|
+
/**
|
|
736
|
+
* Create initial screen shake state.
|
|
737
|
+
*/
|
|
738
|
+
declare function createShakeState(): ScreenShakeState;
|
|
739
|
+
/**
|
|
740
|
+
* Trigger screen shake with duration and intensity.
|
|
741
|
+
*
|
|
742
|
+
* Intensity guide:
|
|
743
|
+
* - 1: Light hit
|
|
744
|
+
* - 2: Medium impact
|
|
745
|
+
* - 3-4: Big explosion
|
|
746
|
+
*/
|
|
747
|
+
declare function triggerShake(state: ScreenShakeState, frames: number, intensity: number): void;
|
|
748
|
+
/**
|
|
749
|
+
* Apply screen shake offset to render position.
|
|
750
|
+
* Call in render() to get adjusted x/y coordinates.
|
|
751
|
+
* Returns { offsetX, offsetY } to add to your render position.
|
|
752
|
+
*/
|
|
753
|
+
declare function applyShake(state: ScreenShakeState): {
|
|
754
|
+
offsetX: number;
|
|
755
|
+
offsetY: number;
|
|
756
|
+
};
|
|
757
|
+
/**
|
|
758
|
+
* Create initial flash state.
|
|
759
|
+
*/
|
|
760
|
+
declare function createFlashState(): FlashState;
|
|
761
|
+
/**
|
|
762
|
+
* Trigger a flash effect for N frames.
|
|
763
|
+
*/
|
|
764
|
+
declare function triggerFlash(state: FlashState, frames: number): void;
|
|
765
|
+
/**
|
|
766
|
+
* Update flash state (decrement). Call once per frame.
|
|
767
|
+
* Returns true if flash is currently active.
|
|
768
|
+
*/
|
|
769
|
+
declare function updateFlash(state: FlashState): boolean;
|
|
770
|
+
/**
|
|
771
|
+
* Check if flash should show on this frame (alternating visibility for strobe).
|
|
772
|
+
* Use with border or hit flash to create a blinking effect.
|
|
773
|
+
*/
|
|
774
|
+
declare function isFlashVisible(state: FlashState): boolean;
|
|
775
|
+
|
|
666
776
|
/**
|
|
667
777
|
* Game registry with metadata
|
|
668
778
|
*/
|
|
@@ -692,4 +802,4 @@ declare function runGame(id: string, terminal: _xterm_xterm.Terminal): {
|
|
|
692
802
|
isRunning: boolean;
|
|
693
803
|
} | undefined;
|
|
694
804
|
|
|
695
|
-
export { GAME_EVENTS, type GameInfo, type GamesMenuController, type GamesMenuOptions, type HackController, MODE_SELECT_ITEMS, type MatrixController, type MenuItem, type MenuState, PAUSE_MENU_ITEMS, PhosphorMode, type RebootController, type RenderMenuOptions, type SimpleMenuItem, checkShortcut, createGameOverMenuItems, createMenuState, createModeSelectMenuItems, createPauseMenuItems, dispatchGameQuit, dispatchGameSwitch, dispatchGamesMenu, dispatchLaunchGame, enterAlternateBuffer, exitAlternateBuffer, forceExitAlternateBuffer, games, getActiveMatrixController, getCurrentThemeColor, getGame, getRandomGame, getSubtleBackgroundColor, getTheme, getThemeColorCode, getVerticalAnchor, handleMatrixKeypress, handleMenuInput, isInAlternateBuffer, isLightTheme, isMatrixWaitingForKey, isTerminalValid, menuConfirm, menuDown, menuReset, menuUp, navigateMenu, playBootTransition, playExitTransition, playQuickBoot, playSelectTransition, playSwitchTransition, renderMenu, renderSimpleMenu, run2048Game, runAsteroidsGame, runBreakoutGame, runCourierGame, runCrackGame, runFroggerGame, runGame, runHackEffect, runHangmanGame, runMatrixEffect, runMinesweeperGame, runPongGame, runRebootEffect, runRunnerGame, runSimonGame, runSnakeGame, runSpaceInvadersGame, runTetrisGame, runTowerGame, runTronGame, runTypingTest, runWordleGame, setTheme, showGamesMenu, startMatrixRain };
|
|
805
|
+
export { FIREWORK_COLORS, type FlashState, GAME_EVENTS, type GameInfo, type GamesMenuController, type GamesMenuOptions, type HackController, MAX_PARTICLES, MODE_SELECT_ITEMS, type MatrixController, type MenuItem, type MenuState, PARTICLE_CHARS, PAUSE_MENU_ITEMS, type Particle, PhosphorMode, type RebootController, type RenderMenuOptions, type ScorePopup, type ScreenShakeState, type SimpleMenuItem, addScorePopup, applyShake, checkShortcut, createFlashState, createGameOverMenuItems, createMenuState, createModeSelectMenuItems, createPauseMenuItems, createShakeState, dispatchGameQuit, dispatchGameSwitch, dispatchGamesMenu, dispatchLaunchGame, enterAlternateBuffer, exitAlternateBuffer, forceExitAlternateBuffer, games, getActiveMatrixController, getCurrentThemeColor, getGame, getRandomGame, getSubtleBackgroundColor, getTheme, getThemeColorCode, getVerticalAnchor, handleMatrixKeypress, handleMenuInput, isFlashVisible, isInAlternateBuffer, isLightTheme, isMatrixWaitingForKey, isTerminalValid, menuConfirm, menuDown, menuReset, menuUp, navigateMenu, playBootTransition, playExitTransition, playQuickBoot, playSelectTransition, playSwitchTransition, renderMenu, renderSimpleMenu, run2048Game, runAsteroidsGame, runBreakoutGame, runCourierGame, runCrackGame, runFroggerGame, runGame, runHackEffect, runHangmanGame, runMatrixEffect, runMinesweeperGame, runPongGame, runRebootEffect, runRunnerGame, runSimonGame, runSnakeGame, runSpaceInvadersGame, runTetrisGame, runTowerGame, runTronGame, runTypingTest, runWordleGame, setTheme, showGamesMenu, spawnFirework, spawnParticles, spawnSparkleTrail, startMatrixRain, triggerFlash, triggerShake, updateFlash, updateParticles, updatePopups };
|