terminal-quest 1.0.2 โ 1.1.2
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 +43 -4
- package/dist/App.js +1 -1
- package/dist/data/stories/00-beginner-pc.js +70 -0
- package/dist/data/stories/01-first-server.js +77 -0
- package/dist/data/stories/02-messy-project.js +24 -0
- package/dist/data/stories/03-log-detective.js +4 -0
- package/dist/data/stories/04-deploy-day.js +4 -0
- package/dist/data/stories/05-git-incident.js +5 -0
- package/dist/data/stories/06-pipe-master.js +4 -0
- package/dist/data/stories/07-dangerous-commands.js +4 -0
- package/dist/data/stories/k1-treasure-hunt.js +181 -0
- package/dist/data/types.d.ts +14 -0
- package/dist/engine/CommandFeedback.d.ts +14 -0
- package/dist/engine/CommandFeedback.js +55 -0
- package/dist/engine/commands/grep.js +42 -17
- package/dist/engine/commands/wc.js +50 -21
- package/dist/screens/MissionBriefScreen.js +3 -3
- package/dist/screens/MissionCompleteScreen.d.ts +2 -1
- package/dist/screens/MissionCompleteScreen.js +37 -12
- package/dist/screens/TerminalScreen.d.ts +1 -1
- package/dist/screens/TerminalScreen.js +47 -7
- package/dist/state/useGameState.d.ts +1 -1
- package/dist/state/useGameState.js +5 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ export function grep(fs, args) {
|
|
|
9
9
|
let ignoreCase = false;
|
|
10
10
|
let showLineNumbers = false;
|
|
11
11
|
let recursive = false;
|
|
12
|
+
let countOnly = false;
|
|
12
13
|
const nonFlagArgs = [];
|
|
13
14
|
for (const arg of args) {
|
|
14
15
|
if (arg.startsWith('-') && arg.length > 1 && !arg.startsWith('--')) {
|
|
@@ -19,6 +20,8 @@ export function grep(fs, args) {
|
|
|
19
20
|
showLineNumbers = true;
|
|
20
21
|
else if (ch === 'r')
|
|
21
22
|
recursive = true;
|
|
23
|
+
else if (ch === 'c')
|
|
24
|
+
countOnly = true;
|
|
22
25
|
else
|
|
23
26
|
return { output: '', error: `grep: invalid option -- '${ch}'` };
|
|
24
27
|
}
|
|
@@ -47,16 +50,23 @@ export function grep(fs, args) {
|
|
|
47
50
|
}
|
|
48
51
|
const lines = stdin.split('\n');
|
|
49
52
|
const results = [];
|
|
53
|
+
let matchCount = 0;
|
|
50
54
|
for (let i = 0; i < lines.length; i++) {
|
|
51
55
|
if (regex.test(lines[i])) {
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
line
|
|
56
|
+
matchCount++;
|
|
57
|
+
if (!countOnly) {
|
|
58
|
+
let line = '';
|
|
59
|
+
if (showLineNumbers) {
|
|
60
|
+
line += `${i + 1}:`;
|
|
61
|
+
}
|
|
62
|
+
line += lines[i];
|
|
63
|
+
results.push(line);
|
|
55
64
|
}
|
|
56
|
-
line += lines[i];
|
|
57
|
-
results.push(line);
|
|
58
65
|
}
|
|
59
66
|
}
|
|
67
|
+
if (countOnly) {
|
|
68
|
+
return { output: String(matchCount) };
|
|
69
|
+
}
|
|
60
70
|
return { output: results.join('\n') };
|
|
61
71
|
}
|
|
62
72
|
const flags = ignoreCase ? 'i' : '';
|
|
@@ -68,27 +78,35 @@ export function grep(fs, args) {
|
|
|
68
78
|
return { output: '', error: `grep: invalid regular expression '${pattern}'` };
|
|
69
79
|
}
|
|
70
80
|
const results = [];
|
|
81
|
+
const fileCounts = [];
|
|
71
82
|
const multipleFiles = targets.length > 1 || recursive;
|
|
72
83
|
function searchFile(filePath) {
|
|
73
84
|
try {
|
|
74
85
|
const content = fs.readFile(filePath);
|
|
75
86
|
const lines = content.split('\n');
|
|
87
|
+
let fileMatchCount = 0;
|
|
76
88
|
for (let i = 0; i < lines.length; i++) {
|
|
77
89
|
if (regex.test(lines[i])) {
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
line
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
fileMatchCount++;
|
|
91
|
+
if (!countOnly) {
|
|
92
|
+
let line = '';
|
|
93
|
+
if (multipleFiles) {
|
|
94
|
+
line += `${filePath}:`;
|
|
95
|
+
}
|
|
96
|
+
if (showLineNumbers) {
|
|
97
|
+
line += `${i + 1}:`;
|
|
98
|
+
}
|
|
99
|
+
line += lines[i];
|
|
100
|
+
results.push(line);
|
|
84
101
|
}
|
|
85
|
-
line += lines[i];
|
|
86
|
-
results.push(line);
|
|
87
102
|
}
|
|
88
103
|
}
|
|
104
|
+
if (countOnly) {
|
|
105
|
+
fileCounts.push({ path: filePath, count: fileMatchCount });
|
|
106
|
+
}
|
|
89
107
|
}
|
|
90
|
-
catch {
|
|
91
|
-
|
|
108
|
+
catch (e) {
|
|
109
|
+
results.push(`grep: ${filePath}: ${e instanceof Error ? e.message : 'cannot read file'}`);
|
|
92
110
|
}
|
|
93
111
|
}
|
|
94
112
|
function searchRecursive(dirPath) {
|
|
@@ -104,8 +122,8 @@ export function grep(fs, args) {
|
|
|
104
122
|
}
|
|
105
123
|
}
|
|
106
124
|
}
|
|
107
|
-
catch {
|
|
108
|
-
|
|
125
|
+
catch (e) {
|
|
126
|
+
results.push(`grep: ${dirPath}: ${e instanceof Error ? e.message : 'cannot read directory'}`);
|
|
109
127
|
}
|
|
110
128
|
}
|
|
111
129
|
for (const target of targets) {
|
|
@@ -122,6 +140,13 @@ export function grep(fs, args) {
|
|
|
122
140
|
return { output: '', error: `grep: ${target}: Is a directory` };
|
|
123
141
|
}
|
|
124
142
|
}
|
|
143
|
+
if (countOnly) {
|
|
144
|
+
if (multipleFiles) {
|
|
145
|
+
return { output: fileCounts.map(fc => `${fc.path}:${fc.count}`).join('\n') };
|
|
146
|
+
}
|
|
147
|
+
const total = fileCounts.reduce((sum, fc) => sum + fc.count, 0);
|
|
148
|
+
return { output: String(total) };
|
|
149
|
+
}
|
|
125
150
|
return { output: results.join('\n') };
|
|
126
151
|
}
|
|
127
152
|
//# sourceMappingURL=grep.js.map
|
|
@@ -33,35 +33,64 @@ export function wc(fs, args) {
|
|
|
33
33
|
showWords = true;
|
|
34
34
|
showBytes = true;
|
|
35
35
|
}
|
|
36
|
-
let content;
|
|
37
|
-
let filename;
|
|
38
36
|
if (files.length > 0) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
const outputLines = [];
|
|
38
|
+
let totalLines = 0;
|
|
39
|
+
let totalWords = 0;
|
|
40
|
+
let totalBytes = 0;
|
|
41
|
+
for (const filename of files) {
|
|
42
|
+
let content;
|
|
43
|
+
try {
|
|
44
|
+
content = fs.readFile(filename);
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return { output: '', error: `wc: ${e.message}` };
|
|
48
|
+
}
|
|
49
|
+
const lines = content === '' ? 0 : (content.match(/\n/g) || []).length;
|
|
50
|
+
const words = content === '' ? 0 : content.split(/\s+/).filter(Boolean).length;
|
|
51
|
+
const bytes = new TextEncoder().encode(content).length;
|
|
52
|
+
totalLines += lines;
|
|
53
|
+
totalWords += words;
|
|
54
|
+
totalBytes += bytes;
|
|
55
|
+
const parts = [];
|
|
56
|
+
if (showLines)
|
|
57
|
+
parts.push(String(lines));
|
|
58
|
+
if (showWords)
|
|
59
|
+
parts.push(String(words));
|
|
60
|
+
if (showBytes)
|
|
61
|
+
parts.push(String(bytes));
|
|
62
|
+
parts.push(filename);
|
|
63
|
+
outputLines.push(parts.join(' '));
|
|
42
64
|
}
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
if (files.length > 1) {
|
|
66
|
+
const totalParts = [];
|
|
67
|
+
if (showLines)
|
|
68
|
+
totalParts.push(String(totalLines));
|
|
69
|
+
if (showWords)
|
|
70
|
+
totalParts.push(String(totalWords));
|
|
71
|
+
if (showBytes)
|
|
72
|
+
totalParts.push(String(totalBytes));
|
|
73
|
+
totalParts.push('total');
|
|
74
|
+
outputLines.push(totalParts.join(' '));
|
|
45
75
|
}
|
|
76
|
+
return { output: outputLines.join('\n') };
|
|
46
77
|
}
|
|
47
78
|
else if (stdin !== undefined) {
|
|
48
|
-
content = stdin;
|
|
79
|
+
const content = stdin;
|
|
80
|
+
const lines = content === '' ? 0 : (content.match(/\n/g) || []).length;
|
|
81
|
+
const words = content === '' ? 0 : content.split(/\s+/).filter(Boolean).length;
|
|
82
|
+
const bytes = new TextEncoder().encode(content).length;
|
|
83
|
+
const parts = [];
|
|
84
|
+
if (showLines)
|
|
85
|
+
parts.push(String(lines));
|
|
86
|
+
if (showWords)
|
|
87
|
+
parts.push(String(words));
|
|
88
|
+
if (showBytes)
|
|
89
|
+
parts.push(String(bytes));
|
|
90
|
+
return { output: parts.join(' ') };
|
|
49
91
|
}
|
|
50
92
|
else {
|
|
51
93
|
return { output: '', error: 'wc: missing file operand' };
|
|
52
94
|
}
|
|
53
|
-
const lines = content === '' ? 0 : (content.match(/\n/g) || []).length;
|
|
54
|
-
const words = content === '' ? 0 : content.split(/\s+/).filter(Boolean).length;
|
|
55
|
-
const bytes = new TextEncoder().encode(content).length;
|
|
56
|
-
const parts = [];
|
|
57
|
-
if (showLines)
|
|
58
|
-
parts.push(String(lines));
|
|
59
|
-
if (showWords)
|
|
60
|
-
parts.push(String(words));
|
|
61
|
-
if (showBytes)
|
|
62
|
-
parts.push(String(bytes));
|
|
63
|
-
if (filename)
|
|
64
|
-
parts.push(filename);
|
|
65
|
-
return { output: parts.join(' ') };
|
|
66
95
|
}
|
|
67
96
|
//# sourceMappingURL=wc.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
3
|
import { colors } from '../utils/colors.js';
|
|
4
4
|
import { stories } from '../data/stories/index.js';
|
|
@@ -15,13 +15,13 @@ export function MissionBriefScreen({ storyId, missionIndex, onNavigate }) {
|
|
|
15
15
|
}
|
|
16
16
|
});
|
|
17
17
|
if (!story || !mission) {
|
|
18
|
-
return
|
|
18
|
+
return _jsxs(Text, { color: colors.error, children: ["\u30DF\u30C3\u30B7\u30E7\u30F3\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (story=", storyId, ", mission=", missionIndex, ")"] });
|
|
19
19
|
}
|
|
20
20
|
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: colors.secondary, children: [story.emoji, " ", story.title, " - \u30DF\u30C3\u30B7\u30E7\u30F3 ", missionIndex + 1, "/", story.missions.length] }) }), _jsxs(Box, { borderStyle: "double", borderColor: colors.primary, paddingX: 2, paddingY: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.primary, children: mission.title }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.narrative, children: mission.narrative }) })] }), mission.newCommands && mission.newCommands.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.primary, children: "\uD83D\uDCD6 \u65B0\u3057\u3044\u30B3\u30DE\u30F3\u30C9:" }), mission.newCommands.map(cmdName => {
|
|
21
21
|
const meta = getCommandMeta(cmdName);
|
|
22
22
|
if (!meta)
|
|
23
23
|
return null;
|
|
24
24
|
return (_jsxs(Box, { marginLeft: 2, marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.secondary, children: meta.name }), _jsxs(Text, { color: colors.file, children: [" ", meta.description] }), meta.examples.map((ex, i) => (_jsxs(Text, { color: colors.muted, children: [' ', "$ ", ex.cmd.padEnd(28), " ", ex.desc] }, i)))] }, cmdName));
|
|
25
|
-
})] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.secondary, children: "\u76EE\u6A19:" }), mission.objectives.map((obj, i) => (_jsx(Text, { color: colors.file, children: ` ${i + 1}. ${obj.description}` }, obj.id)))] }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.muted, children: "Enter\u3067\u30B9\u30BF\u30FC\u30C8\u3001Esc\u3067\u623B\u308B" }) })] }));
|
|
25
|
+
})] })), mission.goal && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.primary, children: ["\uD83C\uDFAF \u5230\u9054\u76EE\u6A19: ", mission.goal] }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.secondary, children: "\u76EE\u6A19:" }), mission.objectives.map((obj, i) => (_jsx(Text, { color: colors.file, children: ` ${i + 1}. ${obj.description}` }, obj.id)))] }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.muted, children: "Enter\u3067\u30B9\u30BF\u30FC\u30C8\u3001Esc\u3067\u623B\u308B" }) })] }));
|
|
26
26
|
}
|
|
27
27
|
//# sourceMappingURL=MissionBriefScreen.js.map
|
|
@@ -2,8 +2,9 @@ import type { Screen } from '../data/types.js';
|
|
|
2
2
|
interface MissionCompleteScreenProps {
|
|
3
3
|
storyId: string;
|
|
4
4
|
missionIndex: number;
|
|
5
|
+
commandCount?: number;
|
|
5
6
|
onNavigate: (screen: Screen) => void;
|
|
6
7
|
}
|
|
7
|
-
export declare function MissionCompleteScreen({ storyId, missionIndex, onNavigate }: MissionCompleteScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export declare function MissionCompleteScreen({ storyId, missionIndex, commandCount, onNavigate }: MissionCompleteScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
8
9
|
export {};
|
|
9
10
|
//# sourceMappingURL=MissionCompleteScreen.d.ts.map
|
|
@@ -1,30 +1,55 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { Box, Text, useInput } from 'ink';
|
|
3
4
|
import { colors } from '../utils/colors.js';
|
|
4
5
|
import { missionCompleteArt, storyCompleteArt } from '../utils/ascii-art.js';
|
|
5
6
|
import { stories } from '../data/stories/index.js';
|
|
6
|
-
export function MissionCompleteScreen({ storyId, missionIndex, onNavigate }) {
|
|
7
|
+
export function MissionCompleteScreen({ storyId, missionIndex, commandCount, onNavigate }) {
|
|
7
8
|
const story = stories.find(s => s.id === storyId);
|
|
8
9
|
const mission = story?.missions[missionIndex];
|
|
9
10
|
const isLastMission = story ? missionIndex >= story.missions.length - 1 : false;
|
|
11
|
+
const review = mission?.review;
|
|
12
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
13
|
+
const [reviewState, setReviewState] = useState(review ? 'answering' : 'done');
|
|
14
|
+
const [wasCorrect, setWasCorrect] = useState(false);
|
|
10
15
|
useInput((_input, key) => {
|
|
11
|
-
if (key.
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
if (key.escape) {
|
|
17
|
+
onNavigate({ type: 'storySelect' });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (reviewState === 'answering' && review) {
|
|
21
|
+
if (key.upArrow) {
|
|
22
|
+
setSelectedIndex(prev => (prev > 0 ? prev - 1 : review.choices.length - 1));
|
|
14
23
|
}
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
if (key.downArrow) {
|
|
25
|
+
setSelectedIndex(prev => (prev < review.choices.length - 1 ? prev + 1 : 0));
|
|
26
|
+
}
|
|
27
|
+
if (key.return) {
|
|
28
|
+
setWasCorrect(selectedIndex === review.correctIndex);
|
|
29
|
+
setReviewState('showResult');
|
|
17
30
|
}
|
|
18
31
|
}
|
|
19
|
-
if (
|
|
20
|
-
|
|
32
|
+
else if (reviewState === 'showResult') {
|
|
33
|
+
if (key.return) {
|
|
34
|
+
setReviewState('done');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (reviewState === 'done') {
|
|
38
|
+
if (key.return) {
|
|
39
|
+
if (isLastMission) {
|
|
40
|
+
onNavigate({ type: 'storySelect' });
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
onNavigate({ type: 'missionBrief', storyId, missionIndex: missionIndex + 1 });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
21
46
|
}
|
|
22
47
|
});
|
|
23
48
|
if (!story || !mission) {
|
|
24
|
-
return
|
|
49
|
+
return _jsxs(Text, { color: colors.error, children: ["\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (story=", storyId, ", mission=", missionIndex, ")"] });
|
|
25
50
|
}
|
|
26
|
-
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", paddingX: 2, children: [_jsx(Text, { color: colors.success, bold: true, children: isLastMission ? storyCompleteArt : missionCompleteArt }), _jsx(Text, { bold: true, color: colors.primary, children: mission.title }), isLastMission && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.secondary, bold: true, children: ["\u300C", story.title, "\u300D\u3092\u30AF\u30EA\u30A2\u3057\u307E\u3057\u305F\uFF01"] }) })), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.muted, children: isLastMission
|
|
51
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", paddingX: 2, children: [_jsx(Text, { color: colors.success, bold: true, children: isLastMission ? storyCompleteArt : missionCompleteArt }), _jsx(Text, { bold: true, color: colors.primary, children: mission.title }), commandCount != null && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, children: ["\u30B3\u30DE\u30F3\u30C9\u5B9F\u884C\u56DE\u6570: ", commandCount, "\u56DE"] }) })), isLastMission && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.secondary, bold: true, children: ["\u300C", story.title, "\u300D\u3092\u30AF\u30EA\u30A2\u3057\u307E\u3057\u305F\uFF01"] }) })), reviewState === 'answering' && review && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: colors.secondary, bold: true, children: "\uD83D\uDCDD \u3075\u308A\u304B\u3048\u308A\u554F\u984C" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: review.question }) }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: review.choices.map((choice, i) => (_jsxs(Text, { color: i === selectedIndex ? colors.primary : colors.muted, children: [i === selectedIndex ? 'โธ ' : ' ', choice] }, i))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "\u2191\u2193\u3067\u9078\u629E\u3001Enter\u3067\u56DE\u7B54" }) })] })), reviewState === 'showResult' && review && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: wasCorrect ? colors.success : colors.error, bold: true, children: wasCorrect ? 'โญ ๆญฃ่งฃ๏ผ' : 'โ ไธๆญฃ่งฃ' }), !wasCorrect && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.primary, children: ["\u6B63\u89E3: ", review.choices[review.correctIndex]] }) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, children: ["\uD83D\uDCA1 ", review.explanation] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "Enter\u3067\u7D9A\u3051\u308B" }) })] })), reviewState === 'done' && (_jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.muted, children: isLastMission
|
|
27
52
|
? 'Enterใงในใใผใชใผ้ธๆใซๆปใ'
|
|
28
|
-
: 'Enterใงๆฌกใฎใใใทใงใณใธ' }) })] }));
|
|
53
|
+
: 'Enterใงๆฌกใฎใใใทใงใณใธ' }) }))] }));
|
|
29
54
|
}
|
|
30
55
|
//# sourceMappingURL=MissionCompleteScreen.js.map
|
|
@@ -3,7 +3,7 @@ interface TerminalScreenProps {
|
|
|
3
3
|
storyId: string;
|
|
4
4
|
missionIndex: number;
|
|
5
5
|
onNavigate: (screen: Screen) => void;
|
|
6
|
-
onMissionComplete: (storyId: string, missionId: string, hintsUsed: number) => void;
|
|
6
|
+
onMissionComplete: (storyId: string, missionId: string, hintsUsed: number, commandCount: number) => void;
|
|
7
7
|
onStoryComplete: (storyId: string) => void;
|
|
8
8
|
onCommandExecuted: () => void;
|
|
9
9
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useCallback } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import { colors } from '../utils/colors.js';
|
|
@@ -11,9 +11,11 @@ import { TerminalPrompt } from '../components/TerminalPrompt.js';
|
|
|
11
11
|
import { TerminalOutput } from '../components/TerminalOutput.js';
|
|
12
12
|
import { ObjectivePanel } from '../components/ObjectivePanel.js';
|
|
13
13
|
import { HintBar } from '../components/HintBar.js';
|
|
14
|
+
import { suggestCommand, checkMissionFeedback } from '../engine/CommandFeedback.js';
|
|
14
15
|
export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionComplete, onStoryComplete, onCommandExecuted, }) {
|
|
15
16
|
const story = stories.find(s => s.id === storyId);
|
|
16
17
|
const mission = story?.missions[missionIndex];
|
|
18
|
+
const course = story?.course;
|
|
17
19
|
const [missionEngine] = useState(() => {
|
|
18
20
|
if (!mission)
|
|
19
21
|
return null;
|
|
@@ -30,11 +32,20 @@ export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionCom
|
|
|
30
32
|
return null;
|
|
31
33
|
return new TabCompletion(missionEngine.getFS());
|
|
32
34
|
});
|
|
33
|
-
const [outputLines, setOutputLines] = useState(
|
|
35
|
+
const [outputLines, setOutputLines] = useState(() => {
|
|
36
|
+
if (course === 'kids') {
|
|
37
|
+
return [{ text: '๐ก ใณใใณใใใซใ
ใใใใใใฆ Enter ใญใผใใใใฆใญใTab ใญใผใงใใฉใใปใใใงใใใใ', type: 'system' }];
|
|
38
|
+
}
|
|
39
|
+
if (course === 'beginner') {
|
|
40
|
+
return [{ text: '๐ก ใณใใณใใๅ
ฅๅใใฆ Enter ใๆผใใฆใใ ใใใTab ใญใผใง่ฃๅฎใโโใญใผใงๅฑฅๆญดใๅผใณๅบใใพใใ', type: 'system' }];
|
|
41
|
+
}
|
|
42
|
+
return [];
|
|
43
|
+
});
|
|
34
44
|
const [commandHistory, setCommandHistory] = useState([]);
|
|
35
45
|
const [completedObjectives, setCompletedObjectives] = useState([]);
|
|
36
46
|
const [currentHint, setCurrentHint] = useState(null);
|
|
37
47
|
const [hintLevel, setHintLevel] = useState(0);
|
|
48
|
+
const [commandCount, setCommandCount] = useState(0);
|
|
38
49
|
useInput((input, key) => {
|
|
39
50
|
if (key.escape) {
|
|
40
51
|
onNavigate({ type: 'storySelect' });
|
|
@@ -61,6 +72,7 @@ export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionCom
|
|
|
61
72
|
if (!trimmed || !commandHandler || !missionEngine || !mission)
|
|
62
73
|
return;
|
|
63
74
|
setCommandHistory(prev => [...prev, trimmed]);
|
|
75
|
+
setCommandCount(prev => prev + 1);
|
|
64
76
|
onCommandExecuted();
|
|
65
77
|
const cwd = missionEngine.getFS().getCwd();
|
|
66
78
|
setOutputLines(prev => [
|
|
@@ -90,7 +102,19 @@ export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionCom
|
|
|
90
102
|
return;
|
|
91
103
|
}
|
|
92
104
|
if (result.error) {
|
|
93
|
-
|
|
105
|
+
let errorText = result.error;
|
|
106
|
+
if (course === 'kids' && errorText.endsWith(': command not found')) {
|
|
107
|
+
const cmdName = errorText.replace(': command not found', '');
|
|
108
|
+
errorText = `ใ${cmdName}ใใจใใใณใใณใใฏใชใใใใใใใกใฉใใใใใฆใฟใฆใญใ`;
|
|
109
|
+
}
|
|
110
|
+
setOutputLines(prev => [...prev, { text: errorText, type: 'error' }]);
|
|
111
|
+
// Command suggestion for typos
|
|
112
|
+
if (result.error.endsWith(': command not found')) {
|
|
113
|
+
const suggestion = suggestCommand(trimmed);
|
|
114
|
+
if (suggestion) {
|
|
115
|
+
setOutputLines(prev => [...prev, { text: `๐ก ใใใใใฆ: ${suggestion}`, type: 'system' }]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
94
118
|
}
|
|
95
119
|
else if (result.output) {
|
|
96
120
|
const lines = result.output.split('\n');
|
|
@@ -99,6 +123,17 @@ export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionCom
|
|
|
99
123
|
...lines.map(line => ({ text: line, type: 'output' })),
|
|
100
124
|
]);
|
|
101
125
|
}
|
|
126
|
+
// Mission-specific feedback (check for all commands, not just errors)
|
|
127
|
+
const currentObjIndex = missionEngine.getCurrentObjectiveIndex();
|
|
128
|
+
if (currentObjIndex < mission.objectives.length) {
|
|
129
|
+
const obj = mission.objectives[currentObjIndex];
|
|
130
|
+
if (obj.feedbacks) {
|
|
131
|
+
const feedback = checkMissionFeedback(trimmed, obj.feedbacks);
|
|
132
|
+
if (feedback) {
|
|
133
|
+
setOutputLines(prev => [...prev, { text: `๐ก ${feedback}`, type: 'system' }]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
102
137
|
const parts = trimmed.split(/\s+/);
|
|
103
138
|
const cmd = parts[0];
|
|
104
139
|
const args = parts.slice(1);
|
|
@@ -117,12 +152,12 @@ export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionCom
|
|
|
117
152
|
setCurrentHint(null);
|
|
118
153
|
if (missionEngine.isAllComplete()) {
|
|
119
154
|
setTimeout(() => {
|
|
120
|
-
onMissionComplete(storyId, mission.id, hintEngine.getTotalHintsUsed());
|
|
155
|
+
onMissionComplete(storyId, mission.id, hintEngine.getTotalHintsUsed(), commandCount + 1);
|
|
121
156
|
const isLast = story ? missionIndex >= story.missions.length - 1 : false;
|
|
122
157
|
if (isLast) {
|
|
123
158
|
onStoryComplete(storyId);
|
|
124
159
|
}
|
|
125
|
-
onNavigate({ type: 'missionComplete', storyId, missionIndex });
|
|
160
|
+
onNavigate({ type: 'missionComplete', storyId, missionIndex, commandCount: commandCount + 1 });
|
|
126
161
|
}, 500);
|
|
127
162
|
}
|
|
128
163
|
}
|
|
@@ -134,6 +169,7 @@ export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionCom
|
|
|
134
169
|
missionIndex,
|
|
135
170
|
story,
|
|
136
171
|
completedObjectives,
|
|
172
|
+
commandCount,
|
|
137
173
|
hintEngine,
|
|
138
174
|
onCommandExecuted,
|
|
139
175
|
onMissionComplete,
|
|
@@ -142,9 +178,13 @@ export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionCom
|
|
|
142
178
|
handleHintRequest,
|
|
143
179
|
]);
|
|
144
180
|
if (!story || !mission || !missionEngine) {
|
|
145
|
-
return
|
|
181
|
+
return _jsxs(Text, { color: colors.error, children: ["\u30DF\u30C3\u30B7\u30E7\u30F3\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (story=", storyId, ", mission=", missionIndex, ")"] });
|
|
146
182
|
}
|
|
147
183
|
const currentObj = mission.objectives[missionEngine.getCurrentObjectiveIndex()];
|
|
148
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: colors.secondary, children: [story.emoji, " ", mission.title] }), _jsx(Text, { color: colors.muted, children:
|
|
184
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: colors.secondary, children: [story.emoji, " ", mission.title] }), _jsx(Text, { color: colors.muted, children: course === 'kids'
|
|
185
|
+
? 'Esc: ใใฉใ | Tab: ใปใใ | hint: ใใณใ | obj: ใใใฒใใ'
|
|
186
|
+
: course === 'beginner'
|
|
187
|
+
? 'Esc: ๆปใ | Tab: ่ฃๅฎ | Ctrl+H: ใใณใ | hint: ใใณใ | obj: ็ฎๆจไธ่ฆง'
|
|
188
|
+
: 'Esc: ๆปใ | Tab: ่ฃๅฎ | Ctrl+H: ใใณใ | hint: ใใณใ | obj: ็ฎๆจ' })] }), _jsx(ObjectivePanel, { objectives: mission.objectives, completedIds: completedObjectives }), currentHint && currentObj && (_jsx(HintBar, { hint: currentHint, currentLevel: hintLevel, maxLevel: currentObj.hints.length })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(TerminalOutput, { lines: outputLines, maxLines: 30 }), _jsx(TerminalPrompt, { cwd: missionEngine.getFS().getCwd(), onSubmit: handleCommand, history: commandHistory, tabCompletion: tabCompletion ?? undefined })] })] }));
|
|
149
189
|
}
|
|
150
190
|
//# sourceMappingURL=TerminalScreen.js.map
|
|
@@ -3,7 +3,7 @@ export declare function useGameState(): {
|
|
|
3
3
|
screen: Screen;
|
|
4
4
|
progress: GameProgress;
|
|
5
5
|
navigateTo: (newScreen: Screen) => void;
|
|
6
|
-
completeMission: (storyId: string, missionId: string, hintsUsed: number) => void;
|
|
6
|
+
completeMission: (storyId: string, missionId: string, hintsUsed: number, commandCount?: number) => void;
|
|
7
7
|
completeStory: (storyId: string) => void;
|
|
8
8
|
incrementCommands: () => void;
|
|
9
9
|
resetAll: () => void;
|
|
@@ -12,7 +12,7 @@ export function useGameState() {
|
|
|
12
12
|
const navigateTo = useCallback((newScreen) => {
|
|
13
13
|
setScreen(newScreen);
|
|
14
14
|
}, []);
|
|
15
|
-
const completeMission = useCallback((storyId, missionId, hintsUsed) => {
|
|
15
|
+
const completeMission = useCallback((storyId, missionId, hintsUsed, commandCount) => {
|
|
16
16
|
setProgress(prev => {
|
|
17
17
|
const storyProg = prev.storyProgress[storyId] ?? {
|
|
18
18
|
storyId,
|
|
@@ -31,6 +31,10 @@ export function useGameState() {
|
|
|
31
31
|
...storyProg.hintsUsed,
|
|
32
32
|
[missionId]: hintsUsed,
|
|
33
33
|
},
|
|
34
|
+
commandsPerMission: {
|
|
35
|
+
...storyProg.commandsPerMission,
|
|
36
|
+
...(commandCount != null ? { [missionId]: commandCount } : {}),
|
|
37
|
+
},
|
|
34
38
|
};
|
|
35
39
|
return {
|
|
36
40
|
...prev,
|
package/package.json
CHANGED