terminal-quest 1.0.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 +21 -0
- package/bin/terminal-quest.js +2 -0
- package/dist/App.d.ts +2 -0
- package/dist/App.js +33 -0
- package/dist/components/HintBar.d.ts +9 -0
- package/dist/components/HintBar.js +11 -0
- package/dist/components/MenuItem.d.ts +9 -0
- package/dist/components/MenuItem.js +9 -0
- package/dist/components/ObjectivePanel.d.ts +8 -0
- package/dist/components/ObjectivePanel.js +10 -0
- package/dist/components/ProgressBar.d.ts +8 -0
- package/dist/components/ProgressBar.js +10 -0
- package/dist/components/TerminalOutput.d.ts +11 -0
- package/dist/components/TerminalOutput.js +25 -0
- package/dist/components/TerminalPrompt.d.ts +10 -0
- package/dist/components/TerminalPrompt.js +46 -0
- package/dist/data/commands-meta.d.ts +17 -0
- package/dist/data/commands-meta.js +256 -0
- package/dist/data/stories/00-beginner-pc.d.ts +3 -0
- package/dist/data/stories/00-beginner-pc.js +841 -0
- package/dist/data/stories/01-first-server.d.ts +3 -0
- package/dist/data/stories/01-first-server.js +364 -0
- package/dist/data/stories/02-messy-project.d.ts +3 -0
- package/dist/data/stories/02-messy-project.js +433 -0
- package/dist/data/stories/03-log-detective.d.ts +3 -0
- package/dist/data/stories/03-log-detective.js +291 -0
- package/dist/data/stories/04-deploy-day.d.ts +3 -0
- package/dist/data/stories/04-deploy-day.js +337 -0
- package/dist/data/stories/05-git-incident.d.ts +3 -0
- package/dist/data/stories/05-git-incident.js +534 -0
- package/dist/data/stories/06-pipe-master.d.ts +3 -0
- package/dist/data/stories/06-pipe-master.js +377 -0
- package/dist/data/stories/07-dangerous-commands.d.ts +3 -0
- package/dist/data/stories/07-dangerous-commands.js +411 -0
- package/dist/data/stories/index.d.ts +4 -0
- package/dist/data/stories/index.js +14 -0
- package/dist/data/stories/k1-treasure-hunt.d.ts +3 -0
- package/dist/data/stories/k1-treasure-hunt.js +815 -0
- package/dist/data/types.d.ts +97 -0
- package/dist/data/types.js +2 -0
- package/dist/engine/Achievements.d.ts +5 -0
- package/dist/engine/Achievements.js +93 -0
- package/dist/engine/CommandHandler.d.ts +17 -0
- package/dist/engine/CommandHandler.js +177 -0
- package/dist/engine/HintEngine.d.ts +10 -0
- package/dist/engine/HintEngine.js +26 -0
- package/dist/engine/MissionEngine.d.ts +17 -0
- package/dist/engine/MissionEngine.js +84 -0
- package/dist/engine/TabCompletion.d.ts +14 -0
- package/dist/engine/TabCompletion.js +93 -0
- package/dist/engine/VirtualFS.d.ts +33 -0
- package/dist/engine/VirtualFS.js +276 -0
- package/dist/engine/commands/cat.d.ts +4 -0
- package/dist/engine/commands/cat.js +18 -0
- package/dist/engine/commands/cd.d.ts +4 -0
- package/dist/engine/commands/cd.js +12 -0
- package/dist/engine/commands/chmod.d.ts +4 -0
- package/dist/engine/commands/chmod.js +98 -0
- package/dist/engine/commands/clear.d.ts +4 -0
- package/dist/engine/commands/clear.js +4 -0
- package/dist/engine/commands/cp.d.ts +4 -0
- package/dist/engine/commands/cp.js +26 -0
- package/dist/engine/commands/cut.d.ts +4 -0
- package/dist/engine/commands/cut.js +76 -0
- package/dist/engine/commands/echo.d.ts +4 -0
- package/dist/engine/commands/echo.js +4 -0
- package/dist/engine/commands/find.d.ts +4 -0
- package/dist/engine/commands/find.js +60 -0
- package/dist/engine/commands/git.d.ts +4 -0
- package/dist/engine/commands/git.js +510 -0
- package/dist/engine/commands/grep.d.ts +4 -0
- package/dist/engine/commands/grep.js +127 -0
- package/dist/engine/commands/head.d.ts +4 -0
- package/dist/engine/commands/head.js +59 -0
- package/dist/engine/commands/help.d.ts +4 -0
- package/dist/engine/commands/help.js +32 -0
- package/dist/engine/commands/hint.d.ts +4 -0
- package/dist/engine/commands/hint.js +4 -0
- package/dist/engine/commands/index.d.ts +8 -0
- package/dist/engine/commands/index.js +51 -0
- package/dist/engine/commands/ls.d.ts +4 -0
- package/dist/engine/commands/ls.js +50 -0
- package/dist/engine/commands/man.d.ts +4 -0
- package/dist/engine/commands/man.js +51 -0
- package/dist/engine/commands/mkdir.d.ts +4 -0
- package/dist/engine/commands/mkdir.js +31 -0
- package/dist/engine/commands/mv.d.ts +4 -0
- package/dist/engine/commands/mv.js +15 -0
- package/dist/engine/commands/pwd.d.ts +4 -0
- package/dist/engine/commands/pwd.js +4 -0
- package/dist/engine/commands/rm.d.ts +4 -0
- package/dist/engine/commands/rm.js +49 -0
- package/dist/engine/commands/sort.d.ts +4 -0
- package/dist/engine/commands/sort.js +100 -0
- package/dist/engine/commands/tail.d.ts +4 -0
- package/dist/engine/commands/tail.js +59 -0
- package/dist/engine/commands/touch.d.ts +4 -0
- package/dist/engine/commands/touch.js +18 -0
- package/dist/engine/commands/uniq.d.ts +4 -0
- package/dist/engine/commands/uniq.js +61 -0
- package/dist/engine/commands/wc.d.ts +4 -0
- package/dist/engine/commands/wc.js +67 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/screens/MissionBriefScreen.d.ts +9 -0
- package/dist/screens/MissionBriefScreen.js +27 -0
- package/dist/screens/MissionCompleteScreen.d.ts +9 -0
- package/dist/screens/MissionCompleteScreen.js +30 -0
- package/dist/screens/ProgressScreen.d.ts +8 -0
- package/dist/screens/ProgressScreen.js +24 -0
- package/dist/screens/SettingsScreen.d.ts +8 -0
- package/dist/screens/SettingsScreen.js +45 -0
- package/dist/screens/StorySelectScreen.d.ts +8 -0
- package/dist/screens/StorySelectScreen.js +81 -0
- package/dist/screens/TerminalScreen.d.ts +12 -0
- package/dist/screens/TerminalScreen.js +150 -0
- package/dist/screens/TitleScreen.d.ts +7 -0
- package/dist/screens/TitleScreen.js +27 -0
- package/dist/state/GameState.d.ts +8 -0
- package/dist/state/GameState.js +12 -0
- package/dist/state/ProgressStore.d.ts +9 -0
- package/dist/state/ProgressStore.js +45 -0
- package/dist/state/useGameState.d.ts +11 -0
- package/dist/state/useGameState.js +92 -0
- package/dist/utils/ascii-art.d.ts +4 -0
- package/dist/utils/ascii-art.js +22 -0
- package/dist/utils/colors.d.ts +17 -0
- package/dist/utils/colors.js +17 -0
- package/dist/utils/text.d.ts +4 -0
- package/dist/utils/text.js +28 -0
- package/package.json +58 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function uniq(fs, args) {
|
|
2
|
+
// Extract stdin
|
|
3
|
+
let stdin;
|
|
4
|
+
const stdinIdx = args.findIndex(a => a.startsWith('__stdin__:'));
|
|
5
|
+
if (stdinIdx !== -1) {
|
|
6
|
+
stdin = args[stdinIdx].slice('__stdin__:'.length);
|
|
7
|
+
args = [...args.slice(0, stdinIdx), ...args.slice(stdinIdx + 1)];
|
|
8
|
+
}
|
|
9
|
+
let showCount = false;
|
|
10
|
+
const files = [];
|
|
11
|
+
for (const arg of args) {
|
|
12
|
+
if (arg.startsWith('-') && arg.length > 1) {
|
|
13
|
+
for (const ch of arg.slice(1)) {
|
|
14
|
+
if (ch === 'c')
|
|
15
|
+
showCount = true;
|
|
16
|
+
else
|
|
17
|
+
return { output: '', error: `uniq: invalid option -- '${ch}'` };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
files.push(arg);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
let content;
|
|
25
|
+
if (files.length > 0) {
|
|
26
|
+
try {
|
|
27
|
+
content = fs.readFile(files[0]);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
return { output: '', error: `uniq: ${e.message}` };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (stdin !== undefined) {
|
|
34
|
+
content = stdin;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
return { output: '', error: 'uniq: missing file operand' };
|
|
38
|
+
}
|
|
39
|
+
const lines = content.split('\n');
|
|
40
|
+
const result = [];
|
|
41
|
+
if (showCount) {
|
|
42
|
+
let i = 0;
|
|
43
|
+
while (i < lines.length) {
|
|
44
|
+
let count = 1;
|
|
45
|
+
while (i + count < lines.length && lines[i + count] === lines[i]) {
|
|
46
|
+
count++;
|
|
47
|
+
}
|
|
48
|
+
result.push(`${count} ${lines[i]}`);
|
|
49
|
+
i += count;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54
|
+
if (i === 0 || lines[i] !== lines[i - 1]) {
|
|
55
|
+
result.push(lines[i]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { output: result.join('\n') };
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=uniq.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export function wc(fs, args) {
|
|
2
|
+
// Extract stdin
|
|
3
|
+
let stdin;
|
|
4
|
+
const stdinIdx = args.findIndex(a => a.startsWith('__stdin__:'));
|
|
5
|
+
if (stdinIdx !== -1) {
|
|
6
|
+
stdin = args[stdinIdx].slice('__stdin__:'.length);
|
|
7
|
+
args = [...args.slice(0, stdinIdx), ...args.slice(stdinIdx + 1)];
|
|
8
|
+
}
|
|
9
|
+
let showLines = false;
|
|
10
|
+
let showWords = false;
|
|
11
|
+
let showBytes = false;
|
|
12
|
+
const files = [];
|
|
13
|
+
for (const arg of args) {
|
|
14
|
+
if (arg.startsWith('-') && arg.length > 1) {
|
|
15
|
+
for (const ch of arg.slice(1)) {
|
|
16
|
+
if (ch === 'l')
|
|
17
|
+
showLines = true;
|
|
18
|
+
else if (ch === 'w')
|
|
19
|
+
showWords = true;
|
|
20
|
+
else if (ch === 'c')
|
|
21
|
+
showBytes = true;
|
|
22
|
+
else
|
|
23
|
+
return { output: '', error: `wc: invalid option -- '${ch}'` };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
files.push(arg);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// If no flags specified, show all
|
|
31
|
+
if (!showLines && !showWords && !showBytes) {
|
|
32
|
+
showLines = true;
|
|
33
|
+
showWords = true;
|
|
34
|
+
showBytes = true;
|
|
35
|
+
}
|
|
36
|
+
let content;
|
|
37
|
+
let filename;
|
|
38
|
+
if (files.length > 0) {
|
|
39
|
+
filename = files[0];
|
|
40
|
+
try {
|
|
41
|
+
content = fs.readFile(filename);
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
return { output: '', error: `wc: ${e.message}` };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (stdin !== undefined) {
|
|
48
|
+
content = stdin;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
return { output: '', error: 'wc: missing file operand' };
|
|
52
|
+
}
|
|
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
|
+
}
|
|
67
|
+
//# sourceMappingURL=wc.js.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Screen } from '../data/types.js';
|
|
2
|
+
interface MissionBriefScreenProps {
|
|
3
|
+
storyId: string;
|
|
4
|
+
missionIndex: number;
|
|
5
|
+
onNavigate: (screen: Screen) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function MissionBriefScreen({ storyId, missionIndex, onNavigate }: MissionBriefScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=MissionBriefScreen.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { colors } from '../utils/colors.js';
|
|
4
|
+
import { stories } from '../data/stories/index.js';
|
|
5
|
+
import { getCommandMeta } from '../data/commands-meta.js';
|
|
6
|
+
export function MissionBriefScreen({ storyId, missionIndex, onNavigate }) {
|
|
7
|
+
const story = stories.find(s => s.id === storyId);
|
|
8
|
+
const mission = story?.missions[missionIndex];
|
|
9
|
+
useInput((_input, key) => {
|
|
10
|
+
if (key.return) {
|
|
11
|
+
onNavigate({ type: 'terminal', storyId, missionIndex });
|
|
12
|
+
}
|
|
13
|
+
if (key.escape) {
|
|
14
|
+
onNavigate({ type: 'storySelect' });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
if (!story || !mission) {
|
|
18
|
+
return _jsx(Text, { color: colors.error, children: "\u30DF\u30C3\u30B7\u30E7\u30F3\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" });
|
|
19
|
+
}
|
|
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
|
+
const meta = getCommandMeta(cmdName);
|
|
22
|
+
if (!meta)
|
|
23
|
+
return null;
|
|
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" }) })] }));
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=MissionBriefScreen.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Screen } from '../data/types.js';
|
|
2
|
+
interface MissionCompleteScreenProps {
|
|
3
|
+
storyId: string;
|
|
4
|
+
missionIndex: number;
|
|
5
|
+
onNavigate: (screen: Screen) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function MissionCompleteScreen({ storyId, missionIndex, onNavigate }: MissionCompleteScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=MissionCompleteScreen.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { colors } from '../utils/colors.js';
|
|
4
|
+
import { missionCompleteArt, storyCompleteArt } from '../utils/ascii-art.js';
|
|
5
|
+
import { stories } from '../data/stories/index.js';
|
|
6
|
+
export function MissionCompleteScreen({ storyId, missionIndex, onNavigate }) {
|
|
7
|
+
const story = stories.find(s => s.id === storyId);
|
|
8
|
+
const mission = story?.missions[missionIndex];
|
|
9
|
+
const isLastMission = story ? missionIndex >= story.missions.length - 1 : false;
|
|
10
|
+
useInput((_input, key) => {
|
|
11
|
+
if (key.return) {
|
|
12
|
+
if (isLastMission) {
|
|
13
|
+
onNavigate({ type: 'storySelect' });
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
onNavigate({ type: 'missionBrief', storyId, missionIndex: missionIndex + 1 });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (key.escape) {
|
|
20
|
+
onNavigate({ type: 'storySelect' });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
if (!story || !mission) {
|
|
24
|
+
return _jsx(Text, { color: colors.error, children: "\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" });
|
|
25
|
+
}
|
|
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
|
|
27
|
+
? 'Enterでストーリー選択に戻る'
|
|
28
|
+
: 'Enterで次のミッションへ' }) })] }));
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=MissionCompleteScreen.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GameProgress, Screen } from '../data/types.js';
|
|
2
|
+
interface ProgressScreenProps {
|
|
3
|
+
progress: GameProgress;
|
|
4
|
+
onNavigate: (screen: Screen) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function ProgressScreen({ progress, onNavigate }: ProgressScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=ProgressScreen.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { colors } from '../utils/colors.js';
|
|
4
|
+
import { ProgressBar } from '../components/ProgressBar.js';
|
|
5
|
+
import { stories } from '../data/stories/index.js';
|
|
6
|
+
import { achievements } from '../engine/Achievements.js';
|
|
7
|
+
export function ProgressScreen({ progress, onNavigate }) {
|
|
8
|
+
useInput((_input, key) => {
|
|
9
|
+
if (key.escape || key.return) {
|
|
10
|
+
onNavigate({ type: 'title' });
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
const totalMissions = stories.reduce((sum, s) => sum + s.missions.length, 0);
|
|
14
|
+
const completedMissions = Object.values(progress.storyProgress).reduce((sum, sp) => sum + sp.completedMissions.length, 0);
|
|
15
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(Text, { bold: true, color: colors.secondary, children: "\u25C6 \u9032\u6357\u72B6\u6CC1 \u25C6" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.file, children: "\u7DCF\u5408\u9032\u6357:" }), _jsx(ProgressBar, { current: completedMissions, total: totalMissions, width: 30 })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: colors.file, children: ["\u5B9F\u884C\u30B3\u30DE\u30F3\u30C9\u6570: ", _jsx(Text, { color: colors.primary, children: progress.totalCommandsExecuted })] }), _jsxs(Text, { color: colors.file, children: ["\u4F7F\u7528\u30D2\u30F3\u30C8\u6570: ", _jsx(Text, { color: colors.primary, children: progress.totalHintsUsed })] }), _jsxs(Text, { color: colors.file, children: ["\u30AF\u30EA\u30A2\u30B9\u30C8\u30FC\u30EA\u30FC: ", _jsxs(Text, { color: colors.primary, children: [progress.completedStories.length, "/", stories.length] })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.secondary, children: "\u30B9\u30C8\u30FC\u30EA\u30FC\u5225:" }), stories.map(story => {
|
|
16
|
+
const sp = progress.storyProgress[story.id];
|
|
17
|
+
const completed = sp?.completedMissions.length ?? 0;
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: colors.file, children: [story.emoji, " ", story.title] }), _jsx(Box, { marginLeft: 2, children: _jsx(ProgressBar, { current: completed, total: story.missions.length, width: 15 }) })] }, story.id));
|
|
19
|
+
})] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.secondary, children: "\u30D0\u30C3\u30B8:" }), achievements.map(badge => {
|
|
20
|
+
const earned = (progress.achievements ?? []).includes(badge.id);
|
|
21
|
+
return (_jsxs(Text, { color: earned ? colors.success : colors.muted, children: [earned ? badge.emoji : '🔒', " ", badge.title, " - ", badge.description] }, badge.id));
|
|
22
|
+
})] }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.muted, children: "Enter/Esc\u3067\u623B\u308B" }) })] }));
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=ProgressScreen.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Screen } from '../data/types.js';
|
|
2
|
+
interface SettingsScreenProps {
|
|
3
|
+
onNavigate: (screen: Screen) => void;
|
|
4
|
+
onReset: () => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function SettingsScreen({ onNavigate, onReset }: SettingsScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=SettingsScreen.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { colors } from '../utils/colors.js';
|
|
5
|
+
import { MenuItem } from '../components/MenuItem.js';
|
|
6
|
+
export function SettingsScreen({ onNavigate, onReset }) {
|
|
7
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
8
|
+
const [confirmReset, setConfirmReset] = useState(false);
|
|
9
|
+
const menuItems = [
|
|
10
|
+
{ label: '進捗をリセット', action: 'reset' },
|
|
11
|
+
{ label: '← タイトルに戻る', action: 'back' },
|
|
12
|
+
];
|
|
13
|
+
useInput((_input, key) => {
|
|
14
|
+
if (confirmReset) {
|
|
15
|
+
if (_input === 'y' || _input === 'Y') {
|
|
16
|
+
onReset();
|
|
17
|
+
setConfirmReset(false);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
setConfirmReset(false);
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (key.upArrow) {
|
|
25
|
+
setSelectedIndex(prev => (prev > 0 ? prev - 1 : menuItems.length - 1));
|
|
26
|
+
}
|
|
27
|
+
if (key.downArrow) {
|
|
28
|
+
setSelectedIndex(prev => (prev < menuItems.length - 1 ? prev + 1 : 0));
|
|
29
|
+
}
|
|
30
|
+
if (key.return) {
|
|
31
|
+
const item = menuItems[selectedIndex];
|
|
32
|
+
if (item.action === 'reset') {
|
|
33
|
+
setConfirmReset(true);
|
|
34
|
+
}
|
|
35
|
+
else if (item.action === 'back') {
|
|
36
|
+
onNavigate({ type: 'title' });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (key.escape) {
|
|
40
|
+
onNavigate({ type: 'title' });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(Text, { bold: true, color: colors.secondary, children: "\u25C6 \u8A2D\u5B9A \u25C6" }), confirmReset ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.warning, bold: true, children: "\u672C\u5F53\u306B\u5168\u3066\u306E\u9032\u6357\u3092\u30EA\u30BB\u30C3\u30C8\u3057\u307E\u3059\u304B\uFF1F" }), _jsx(Text, { color: colors.muted, children: "y: \u306F\u3044 / \u305D\u306E\u4ED6\u306E\u30AD\u30FC: \u3044\u3044\u3048" })] })) : (_jsx(Box, { flexDirection: "column", marginTop: 1, children: menuItems.map((item, i) => (_jsx(MenuItem, { label: item.label, isSelected: i === selectedIndex }, item.label))) })), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.muted, children: "Esc\u3067\u623B\u308B" }) })] }));
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=SettingsScreen.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GameProgress, Screen } from '../data/types.js';
|
|
2
|
+
interface StorySelectScreenProps {
|
|
3
|
+
progress: GameProgress;
|
|
4
|
+
onNavigate: (screen: Screen) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function StorySelectScreen({ progress, onNavigate }: StorySelectScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=StorySelectScreen.d.ts.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { colors } from '../utils/colors.js';
|
|
5
|
+
import { MenuItem } from '../components/MenuItem.js';
|
|
6
|
+
import { ProgressBar } from '../components/ProgressBar.js';
|
|
7
|
+
import { stories } from '../data/stories/index.js';
|
|
8
|
+
import { isStoryUnlocked } from '../state/ProgressStore.js';
|
|
9
|
+
const courseConfig = [
|
|
10
|
+
{ key: 'kids', label: '✨ 小学生向けコース', emoji: '✨' },
|
|
11
|
+
{ key: 'beginner', label: '💻 はじめてコース', emoji: '💻' },
|
|
12
|
+
{ key: 'engineer', label: '🖥️ エンジニアコース', emoji: '🖥️' },
|
|
13
|
+
];
|
|
14
|
+
export function StorySelectScreen({ progress, onNavigate }) {
|
|
15
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
16
|
+
const groupedStories = useMemo(() => {
|
|
17
|
+
const groups = [];
|
|
18
|
+
let flatIndex = 0;
|
|
19
|
+
for (const course of courseConfig) {
|
|
20
|
+
const courseStories = stories.filter(s => s.course === course.key);
|
|
21
|
+
if (courseStories.length === 0)
|
|
22
|
+
continue;
|
|
23
|
+
groups.push({ type: 'header', label: course.label });
|
|
24
|
+
for (const story of courseStories) {
|
|
25
|
+
groups.push({ type: 'story', story, flatIndex });
|
|
26
|
+
flatIndex++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// courseが未設定のストーリーがあれば最後に追加
|
|
30
|
+
const uncategorized = stories.filter(s => !s.course);
|
|
31
|
+
if (uncategorized.length > 0) {
|
|
32
|
+
groups.push({ type: 'header', label: '📚 その他' });
|
|
33
|
+
for (const story of uncategorized) {
|
|
34
|
+
groups.push({ type: 'story', story, flatIndex });
|
|
35
|
+
flatIndex++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { groups, totalStories: flatIndex };
|
|
39
|
+
}, []);
|
|
40
|
+
const { groups, totalStories } = groupedStories;
|
|
41
|
+
useInput((_input, key) => {
|
|
42
|
+
if (key.upArrow) {
|
|
43
|
+
setSelectedIndex(prev => (prev > 0 ? prev - 1 : totalStories));
|
|
44
|
+
}
|
|
45
|
+
if (key.downArrow) {
|
|
46
|
+
setSelectedIndex(prev => (prev < totalStories ? prev + 1 : 0));
|
|
47
|
+
}
|
|
48
|
+
if (key.return) {
|
|
49
|
+
if (selectedIndex === totalStories) {
|
|
50
|
+
onNavigate({ type: 'title' });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const storyItem = groups.find(g => g.type === 'story' && g.flatIndex === selectedIndex);
|
|
54
|
+
if (!storyItem || storyItem.type !== 'story')
|
|
55
|
+
return;
|
|
56
|
+
const story = storyItem.story;
|
|
57
|
+
const unlocked = isStoryUnlocked(progress, story.id, stories);
|
|
58
|
+
if (!unlocked)
|
|
59
|
+
return;
|
|
60
|
+
const storyProg = progress.storyProgress[story.id];
|
|
61
|
+
const missionIndex = storyProg ? storyProg.currentMissionIndex : 0;
|
|
62
|
+
const clampedIndex = Math.min(missionIndex, story.missions.length - 1);
|
|
63
|
+
onNavigate({ type: 'missionBrief', storyId: story.id, missionIndex: clampedIndex });
|
|
64
|
+
}
|
|
65
|
+
if (key.escape) {
|
|
66
|
+
onNavigate({ type: 'title' });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: colors.secondary, children: "\u25C6 \u30B3\u30FC\u30B9\u3092\u9078\u629E \u25C6" }) }), groups.map((item, i) => {
|
|
70
|
+
if (item.type === 'header') {
|
|
71
|
+
return (_jsx(Box, { marginTop: i > 0 ? 1 : 0, marginBottom: 0, children: _jsxs(Text, { bold: true, color: colors.muted, children: ["\u2500\u2500 ", item.label, " \u2500\u2500"] }) }, `header-${i}`));
|
|
72
|
+
}
|
|
73
|
+
const { story, flatIndex } = item;
|
|
74
|
+
const unlocked = isStoryUnlocked(progress, story.id, stories);
|
|
75
|
+
const storyProg = progress.storyProgress[story.id];
|
|
76
|
+
const completed = storyProg?.completedMissions.length ?? 0;
|
|
77
|
+
const total = story.missions.length;
|
|
78
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsx(MenuItem, { label: `${story.emoji} ${story.title}`, isSelected: flatIndex === selectedIndex, isLocked: !unlocked, description: unlocked ? story.description : undefined }), unlocked && (_jsx(Box, { marginLeft: 4, children: _jsx(ProgressBar, { current: completed, total: total, width: 15 }) }))] }, story.id));
|
|
79
|
+
}), _jsx(Box, { marginTop: 1, children: _jsx(MenuItem, { label: "\u2190 \u30BF\u30A4\u30C8\u30EB\u306B\u623B\u308B", isSelected: selectedIndex === totalStories }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "\u2191\u2193\u3067\u9078\u629E\u3001Enter\u3067\u6C7A\u5B9A\u3001Esc\u3067\u623B\u308B" }) })] }));
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=StorySelectScreen.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Screen } from '../data/types.js';
|
|
2
|
+
interface TerminalScreenProps {
|
|
3
|
+
storyId: string;
|
|
4
|
+
missionIndex: number;
|
|
5
|
+
onNavigate: (screen: Screen) => void;
|
|
6
|
+
onMissionComplete: (storyId: string, missionId: string, hintsUsed: number) => void;
|
|
7
|
+
onStoryComplete: (storyId: string) => void;
|
|
8
|
+
onCommandExecuted: () => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionComplete, onStoryComplete, onCommandExecuted, }: TerminalScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=TerminalScreen.d.ts.map
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { colors } from '../utils/colors.js';
|
|
5
|
+
import { stories } from '../data/stories/index.js';
|
|
6
|
+
import { CommandHandler } from '../engine/CommandHandler.js';
|
|
7
|
+
import { MissionEngine } from '../engine/MissionEngine.js';
|
|
8
|
+
import { HintEngine } from '../engine/HintEngine.js';
|
|
9
|
+
import { TabCompletion } from '../engine/TabCompletion.js';
|
|
10
|
+
import { TerminalPrompt } from '../components/TerminalPrompt.js';
|
|
11
|
+
import { TerminalOutput } from '../components/TerminalOutput.js';
|
|
12
|
+
import { ObjectivePanel } from '../components/ObjectivePanel.js';
|
|
13
|
+
import { HintBar } from '../components/HintBar.js';
|
|
14
|
+
export function TerminalScreen({ storyId, missionIndex, onNavigate, onMissionComplete, onStoryComplete, onCommandExecuted, }) {
|
|
15
|
+
const story = stories.find(s => s.id === storyId);
|
|
16
|
+
const mission = story?.missions[missionIndex];
|
|
17
|
+
const [missionEngine] = useState(() => {
|
|
18
|
+
if (!mission)
|
|
19
|
+
return null;
|
|
20
|
+
return new MissionEngine(mission);
|
|
21
|
+
});
|
|
22
|
+
const [commandHandler] = useState(() => {
|
|
23
|
+
if (!missionEngine)
|
|
24
|
+
return null;
|
|
25
|
+
return new CommandHandler(missionEngine.getFS());
|
|
26
|
+
});
|
|
27
|
+
const [hintEngine] = useState(() => new HintEngine());
|
|
28
|
+
const [tabCompletion] = useState(() => {
|
|
29
|
+
if (!missionEngine)
|
|
30
|
+
return null;
|
|
31
|
+
return new TabCompletion(missionEngine.getFS());
|
|
32
|
+
});
|
|
33
|
+
const [outputLines, setOutputLines] = useState([]);
|
|
34
|
+
const [commandHistory, setCommandHistory] = useState([]);
|
|
35
|
+
const [completedObjectives, setCompletedObjectives] = useState([]);
|
|
36
|
+
const [currentHint, setCurrentHint] = useState(null);
|
|
37
|
+
const [hintLevel, setHintLevel] = useState(0);
|
|
38
|
+
useInput((input, key) => {
|
|
39
|
+
if (key.escape) {
|
|
40
|
+
onNavigate({ type: 'storySelect' });
|
|
41
|
+
}
|
|
42
|
+
if (input === 'h' && key.ctrl && mission && missionEngine) {
|
|
43
|
+
handleHintRequest();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
const handleHintRequest = useCallback(() => {
|
|
47
|
+
if (!mission || !missionEngine)
|
|
48
|
+
return;
|
|
49
|
+
const currentObjIndex = missionEngine.getCurrentObjectiveIndex();
|
|
50
|
+
if (currentObjIndex >= mission.objectives.length)
|
|
51
|
+
return;
|
|
52
|
+
const obj = mission.objectives[currentObjIndex];
|
|
53
|
+
const hint = hintEngine.getNextHint(obj.id, obj.hints);
|
|
54
|
+
if (hint) {
|
|
55
|
+
setCurrentHint(hint);
|
|
56
|
+
setHintLevel(hintEngine.getCurrentLevel(obj.id));
|
|
57
|
+
}
|
|
58
|
+
}, [mission, missionEngine, hintEngine]);
|
|
59
|
+
const handleCommand = useCallback((input) => {
|
|
60
|
+
const trimmed = input.trim();
|
|
61
|
+
if (!trimmed || !commandHandler || !missionEngine || !mission)
|
|
62
|
+
return;
|
|
63
|
+
setCommandHistory(prev => [...prev, trimmed]);
|
|
64
|
+
onCommandExecuted();
|
|
65
|
+
const cwd = missionEngine.getFS().getCwd();
|
|
66
|
+
setOutputLines(prev => [
|
|
67
|
+
...prev,
|
|
68
|
+
{ text: `${cwd} $ ${trimmed}`, type: 'system' },
|
|
69
|
+
]);
|
|
70
|
+
if (trimmed === 'hint') {
|
|
71
|
+
handleHintRequest();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (trimmed === 'objectives' || trimmed === 'obj') {
|
|
75
|
+
mission.objectives.forEach((obj, i) => {
|
|
76
|
+
const done = completedObjectives.includes(obj.id);
|
|
77
|
+
setOutputLines(prev => [
|
|
78
|
+
...prev,
|
|
79
|
+
{
|
|
80
|
+
text: `${done ? '✓' : '○'} ${i + 1}. ${obj.description}`,
|
|
81
|
+
type: done ? 'success' : 'output',
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const result = commandHandler.execute(trimmed);
|
|
88
|
+
if (result.output === 'CLEAR_SCREEN') {
|
|
89
|
+
setOutputLines([]);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (result.error) {
|
|
93
|
+
setOutputLines(prev => [...prev, { text: result.error, type: 'error' }]);
|
|
94
|
+
}
|
|
95
|
+
else if (result.output) {
|
|
96
|
+
const lines = result.output.split('\n');
|
|
97
|
+
setOutputLines(prev => [
|
|
98
|
+
...prev,
|
|
99
|
+
...lines.map(line => ({ text: line, type: 'output' })),
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
const parts = trimmed.split(/\s+/);
|
|
103
|
+
const cmd = parts[0];
|
|
104
|
+
const args = parts.slice(1);
|
|
105
|
+
const newlyCompleted = missionEngine.checkObjectives(cmd, args, result.output);
|
|
106
|
+
if (newlyCompleted.length > 0) {
|
|
107
|
+
setCompletedObjectives(prev => [...prev, ...newlyCompleted]);
|
|
108
|
+
for (const objId of newlyCompleted) {
|
|
109
|
+
const obj = mission.objectives.find(o => o.id === objId);
|
|
110
|
+
if (obj) {
|
|
111
|
+
setOutputLines(prev => [
|
|
112
|
+
...prev,
|
|
113
|
+
{ text: `✓ 目標達成: ${obj.description}`, type: 'success' },
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
setCurrentHint(null);
|
|
118
|
+
if (missionEngine.isAllComplete()) {
|
|
119
|
+
setTimeout(() => {
|
|
120
|
+
onMissionComplete(storyId, mission.id, hintEngine.getTotalHintsUsed());
|
|
121
|
+
const isLast = story ? missionIndex >= story.missions.length - 1 : false;
|
|
122
|
+
if (isLast) {
|
|
123
|
+
onStoryComplete(storyId);
|
|
124
|
+
}
|
|
125
|
+
onNavigate({ type: 'missionComplete', storyId, missionIndex });
|
|
126
|
+
}, 500);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}, [
|
|
130
|
+
commandHandler,
|
|
131
|
+
missionEngine,
|
|
132
|
+
mission,
|
|
133
|
+
storyId,
|
|
134
|
+
missionIndex,
|
|
135
|
+
story,
|
|
136
|
+
completedObjectives,
|
|
137
|
+
hintEngine,
|
|
138
|
+
onCommandExecuted,
|
|
139
|
+
onMissionComplete,
|
|
140
|
+
onStoryComplete,
|
|
141
|
+
onNavigate,
|
|
142
|
+
handleHintRequest,
|
|
143
|
+
]);
|
|
144
|
+
if (!story || !mission || !missionEngine) {
|
|
145
|
+
return _jsx(Text, { color: colors.error, children: "\u30DF\u30C3\u30B7\u30E7\u30F3\u30C7\u30FC\u30BF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" });
|
|
146
|
+
}
|
|
147
|
+
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: "Esc: \u623B\u308B | Tab: \u88DC\u5B8C | Ctrl+H: \u30D2\u30F3\u30C8 | hint: \u30D2\u30F3\u30C8 | obj: \u76EE\u6A19" })] }), _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
|
+
}
|
|
150
|
+
//# sourceMappingURL=TerminalScreen.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Screen } from '../data/types.js';
|
|
2
|
+
interface TitleScreenProps {
|
|
3
|
+
onNavigate: (screen: Screen) => void;
|
|
4
|
+
}
|
|
5
|
+
export declare function TitleScreen({ onNavigate }: TitleScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=TitleScreen.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { colors } from '../utils/colors.js';
|
|
5
|
+
import { titleArt } from '../utils/ascii-art.js';
|
|
6
|
+
import { MenuItem } from '../components/MenuItem.js';
|
|
7
|
+
const menuItems = [
|
|
8
|
+
{ label: 'ゲームスタート', screen: { type: 'storySelect' } },
|
|
9
|
+
{ label: '進捗を見る', screen: { type: 'progress' } },
|
|
10
|
+
{ label: '設定', screen: { type: 'settings' } },
|
|
11
|
+
];
|
|
12
|
+
export function TitleScreen({ onNavigate }) {
|
|
13
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
14
|
+
useInput((_input, key) => {
|
|
15
|
+
if (key.upArrow) {
|
|
16
|
+
setSelectedIndex(prev => (prev > 0 ? prev - 1 : menuItems.length - 1));
|
|
17
|
+
}
|
|
18
|
+
if (key.downArrow) {
|
|
19
|
+
setSelectedIndex(prev => (prev < menuItems.length - 1 ? prev + 1 : 0));
|
|
20
|
+
}
|
|
21
|
+
if (key.return) {
|
|
22
|
+
onNavigate(menuItems[selectedIndex].screen);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsx(Text, { color: colors.primary, children: titleArt }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: menuItems.map((item, i) => (_jsx(MenuItem, { label: item.label, isSelected: i === selectedIndex }, item.label))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "\u2191\u2193\u3067\u9078\u629E\u3001Enter\u3067\u6C7A\u5B9A\u3001Ctrl+C\u3067\u7D42\u4E86" }) })] }));
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=TitleScreen.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GameProgress, Screen } from '../data/types.js';
|
|
2
|
+
export interface GameState {
|
|
3
|
+
screen: Screen;
|
|
4
|
+
progress: GameProgress;
|
|
5
|
+
}
|
|
6
|
+
export declare const initialProgress: GameProgress;
|
|
7
|
+
export declare const initialGameState: GameState;
|
|
8
|
+
//# sourceMappingURL=GameState.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const initialProgress = {
|
|
2
|
+
completedStories: [],
|
|
3
|
+
storyProgress: {},
|
|
4
|
+
totalCommandsExecuted: 0,
|
|
5
|
+
totalHintsUsed: 0,
|
|
6
|
+
achievements: [],
|
|
7
|
+
};
|
|
8
|
+
export const initialGameState = {
|
|
9
|
+
screen: { type: 'title' },
|
|
10
|
+
progress: initialProgress,
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=GameState.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GameProgress } from '../data/types.js';
|
|
2
|
+
export declare function loadProgress(): GameProgress;
|
|
3
|
+
export declare function saveProgress(progress: GameProgress): void;
|
|
4
|
+
export declare function resetProgress(): void;
|
|
5
|
+
export declare function isStoryUnlocked(progress: GameProgress, storyId: string, allStories: Array<{
|
|
6
|
+
id: string;
|
|
7
|
+
unlockRequires: string[];
|
|
8
|
+
}>): boolean;
|
|
9
|
+
//# sourceMappingURL=ProgressStore.d.ts.map
|