mirror of
https://github.com/tcsenpai/shellquest.git
synced 2025-05-31 16:30:13 +00:00
achievements and map
This commit is contained in:
parent
05bad8b6cf
commit
1ad0f8d871
59
achievements.json
Normal file
59
achievements.json
Normal file
@ -0,0 +1,59 @@
|
||||
[
|
||||
{
|
||||
"id": "first_steps",
|
||||
"name": "First Steps",
|
||||
"description": "Complete your first level",
|
||||
"icon": "🏆",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"id": "speed_demon",
|
||||
"name": "Speed Demon",
|
||||
"description": "Complete a level in under 60 seconds",
|
||||
"icon": "⚡",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"id": "no_hints",
|
||||
"name": "Solo Hacker",
|
||||
"description": "Complete a level without using hints",
|
||||
"icon": "🧠",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"id": "command_master",
|
||||
"name": "Command Master",
|
||||
"description": "Use at least 10 different commands in one level",
|
||||
"icon": "💻",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"id": "persistence",
|
||||
"name": "Persistence",
|
||||
"description": "Try at least 20 commands in a single level",
|
||||
"icon": "🔨",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"id": "explorer",
|
||||
"name": "Explorer",
|
||||
"description": "Visit all directories in a file system level",
|
||||
"icon": "🧭",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"id": "easter_egg_hunter",
|
||||
"name": "Easter Egg Hunter",
|
||||
"description": "Find a hidden secret",
|
||||
"icon": "🥚",
|
||||
"secret": true,
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"id": "master_hacker",
|
||||
"name": "Master Hacker",
|
||||
"description": "Complete the game",
|
||||
"icon": "👑",
|
||||
"unlocked": false
|
||||
}
|
||||
]
|
@ -15,6 +15,7 @@
|
||||
"bun-types": "latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"beep": "^0.0.0",
|
||||
"figlet": "^1.6.0",
|
||||
"figlet-cli": "^0.2.0",
|
||||
"gradient-string": "^2.0.2",
|
||||
|
366
src/core/achievements.ts
Normal file
366
src/core/achievements.ts
Normal file
@ -0,0 +1,366 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { getCurrentGameState } from './gameState';
|
||||
import { getTheme, successAnimation } from '../ui/visualEffects';
|
||||
import { playSound } from '../ui/soundEffects';
|
||||
|
||||
// Define achievement types
|
||||
export interface Achievement {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
secret?: boolean;
|
||||
unlocked: boolean;
|
||||
unlockedAt?: number;
|
||||
}
|
||||
|
||||
// Define all achievements
|
||||
export const achievements: Achievement[] = [
|
||||
{
|
||||
id: 'first_steps',
|
||||
name: 'First Steps',
|
||||
description: 'Complete your first level',
|
||||
icon: '🏆',
|
||||
unlocked: false
|
||||
},
|
||||
{
|
||||
id: 'speed_demon',
|
||||
name: 'Speed Demon',
|
||||
description: 'Complete a level in under 60 seconds',
|
||||
icon: '⚡',
|
||||
unlocked: false
|
||||
},
|
||||
{
|
||||
id: 'no_hints',
|
||||
name: 'Solo Hacker',
|
||||
description: 'Complete a level without using hints',
|
||||
icon: '🧠',
|
||||
unlocked: false
|
||||
},
|
||||
{
|
||||
id: 'command_master',
|
||||
name: 'Command Master',
|
||||
description: 'Use at least 10 different commands in one level',
|
||||
icon: '💻',
|
||||
unlocked: false
|
||||
},
|
||||
{
|
||||
id: 'persistence',
|
||||
name: 'Persistence',
|
||||
description: 'Try at least 20 commands in a single level',
|
||||
icon: '🔨',
|
||||
unlocked: false
|
||||
},
|
||||
{
|
||||
id: 'explorer',
|
||||
name: 'Explorer',
|
||||
description: 'Visit all directories in a file system level',
|
||||
icon: '🧭',
|
||||
unlocked: false
|
||||
},
|
||||
{
|
||||
id: 'easter_egg_hunter',
|
||||
name: 'Easter Egg Hunter',
|
||||
description: 'Find a hidden secret',
|
||||
icon: '🥚',
|
||||
secret: true,
|
||||
unlocked: false
|
||||
},
|
||||
{
|
||||
id: 'master_hacker',
|
||||
name: 'Master Hacker',
|
||||
description: 'Complete the game',
|
||||
icon: '👑',
|
||||
unlocked: false
|
||||
}
|
||||
];
|
||||
|
||||
// Path to achievements file
|
||||
const achievementsPath = path.join(process.cwd(), 'achievements.json');
|
||||
|
||||
// Load achievements from file
|
||||
export async function loadAchievements(): Promise<Achievement[]> {
|
||||
try {
|
||||
const data = await fs.readFile(achievementsPath, 'utf-8');
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
// If file doesn't exist, create it with default achievements
|
||||
await saveAchievements(achievements);
|
||||
return achievements;
|
||||
}
|
||||
}
|
||||
|
||||
// Save achievements to file
|
||||
export async function saveAchievements(achievements: Achievement[]): Promise<void> {
|
||||
try {
|
||||
await fs.writeFile(achievementsPath, JSON.stringify(achievements, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Error saving achievements:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Get player achievements
|
||||
export async function getPlayerAchievements(playerName: string): Promise<Achievement[]> {
|
||||
const allAchievements = await loadAchievements();
|
||||
|
||||
// Filter achievements for this player (in a real game, you'd store player-specific achievements)
|
||||
return allAchievements;
|
||||
}
|
||||
|
||||
// Unlock an achievement
|
||||
export async function unlockAchievement(achievementId: string): Promise<boolean> {
|
||||
const gameState = getCurrentGameState();
|
||||
if (!gameState) return false;
|
||||
|
||||
// Load achievements
|
||||
const allAchievements = await loadAchievements();
|
||||
const achievement = allAchievements.find(a => a.id === achievementId);
|
||||
|
||||
if (!achievement || achievement.unlocked) {
|
||||
return false; // Achievement doesn't exist or is already unlocked
|
||||
}
|
||||
|
||||
// Unlock the achievement
|
||||
achievement.unlocked = true;
|
||||
achievement.unlockedAt = Date.now();
|
||||
|
||||
// Save achievements
|
||||
await saveAchievements(allAchievements);
|
||||
|
||||
// Show achievement notification
|
||||
const theme = getTheme();
|
||||
console.log('\n');
|
||||
console.log('╔' + '═'.repeat(50) + '╗');
|
||||
console.log('║ ' + theme.accent('Achievement Unlocked!').padEnd(48) + ' ║');
|
||||
console.log('║ ' + `${achievement.icon} ${achievement.name}`.padEnd(48) + ' ║');
|
||||
console.log('║ ' + achievement.description.padEnd(48) + ' ║');
|
||||
console.log('╚' + '═'.repeat(50) + '╝');
|
||||
|
||||
// Play sound
|
||||
playSound('success');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check and potentially unlock achievements based on game events
|
||||
export async function checkAchievements(event: string, data?: any): Promise<void> {
|
||||
const gameState = getCurrentGameState();
|
||||
if (!gameState) return;
|
||||
|
||||
switch (event) {
|
||||
case 'level_completed':
|
||||
// First Steps achievement
|
||||
await unlockAchievement('first_steps');
|
||||
|
||||
// Speed Demon achievement
|
||||
const levelState = gameState.levelStates[gameState.currentLevel];
|
||||
if (levelState && levelState.startTime) {
|
||||
const completionTime = Date.now() - levelState.startTime;
|
||||
if (completionTime < 60000) { // Less than 60 seconds
|
||||
await unlockAchievement('speed_demon');
|
||||
}
|
||||
}
|
||||
|
||||
// No Hints achievement
|
||||
if (levelState && !levelState.usedHint) {
|
||||
await unlockAchievement('no_hints');
|
||||
}
|
||||
|
||||
// Command Master achievement
|
||||
if (levelState && levelState.uniqueCommands && levelState.uniqueCommands.size >= 10) {
|
||||
await unlockAchievement('command_master');
|
||||
}
|
||||
|
||||
// Persistence achievement
|
||||
if (levelState && levelState.commandCount && levelState.commandCount >= 20) {
|
||||
await unlockAchievement('persistence');
|
||||
}
|
||||
|
||||
// Master Hacker achievement (game completed)
|
||||
const allLevels = data?.allLevels || [];
|
||||
if (gameState.currentLevel >= allLevels.length) {
|
||||
await unlockAchievement('master_hacker');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'hint_used':
|
||||
// Mark that hints were used for this level
|
||||
const currentLevel = gameState.currentLevel;
|
||||
if (!gameState.levelStates[currentLevel]) {
|
||||
gameState.levelStates[currentLevel] = {};
|
||||
}
|
||||
gameState.levelStates[currentLevel].usedHint = true;
|
||||
break;
|
||||
|
||||
case 'command_used':
|
||||
// Track unique commands used
|
||||
const level = gameState.currentLevel;
|
||||
if (!gameState.levelStates[level]) {
|
||||
gameState.levelStates[level] = {};
|
||||
}
|
||||
|
||||
if (!gameState.levelStates[level].uniqueCommands) {
|
||||
gameState.levelStates[level].uniqueCommands = new Set();
|
||||
}
|
||||
|
||||
if (!gameState.levelStates[level].commandCount) {
|
||||
gameState.levelStates[level].commandCount = 0;
|
||||
}
|
||||
|
||||
gameState.levelStates[level].uniqueCommands.add(data.command);
|
||||
gameState.levelStates[level].commandCount++;
|
||||
break;
|
||||
|
||||
case 'easter_egg_found':
|
||||
await unlockAchievement('easter_egg_hunter');
|
||||
break;
|
||||
|
||||
case 'all_directories_visited':
|
||||
await unlockAchievement('explorer');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Display achievements screen
|
||||
export async function showAchievements(): Promise<void> {
|
||||
const gameState = getCurrentGameState();
|
||||
if (!gameState) return;
|
||||
|
||||
const theme = getTheme();
|
||||
const allAchievements = await loadAchievements();
|
||||
|
||||
console.clear();
|
||||
console.log(theme.accent('=== Achievements ==='));
|
||||
console.log('');
|
||||
|
||||
// Group achievements by unlocked status
|
||||
const unlockedAchievements = allAchievements.filter(a => a.unlocked);
|
||||
const lockedAchievements = allAchievements.filter(a => !a.unlocked && !a.secret);
|
||||
const secretAchievements = allAchievements.filter(a => !a.unlocked && a.secret);
|
||||
|
||||
// Display unlocked achievements
|
||||
console.log(theme.success('Unlocked Achievements:'));
|
||||
if (unlockedAchievements.length === 0) {
|
||||
console.log(' None yet. Keep playing!');
|
||||
} else {
|
||||
unlockedAchievements.forEach(a => {
|
||||
console.log(` ${a.icon} ${theme.accent(a.name)} - ${a.description}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// Display locked achievements
|
||||
console.log(theme.secondary('Locked Achievements:'));
|
||||
if (lockedAchievements.length === 0) {
|
||||
console.log(' You\'ve unlocked all regular achievements!');
|
||||
} else {
|
||||
lockedAchievements.forEach(a => {
|
||||
console.log(` ${a.icon} ${theme.accent(a.name)} - ${a.description}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// Display secret achievements (just show that they exist)
|
||||
console.log(theme.warning('Secret Achievements:'));
|
||||
secretAchievements.forEach(a => {
|
||||
console.log(` ${a.icon} ${theme.accent('???')} - Find this secret achievement!`);
|
||||
});
|
||||
|
||||
console.log('');
|
||||
console.log(`Total Progress: ${unlockedAchievements.length}/${allAchievements.length} achievements unlocked`);
|
||||
}
|
||||
|
||||
// Add this function to the achievements.ts file
|
||||
export async function triggerAchievement(
|
||||
eventType: 'level_completed' | 'hint_used' | 'command_used' | 'easter_egg_found' | 'all_directories_visited',
|
||||
data: any = {}
|
||||
): Promise<void> {
|
||||
const gameState = getCurrentGameState();
|
||||
if (!gameState) return;
|
||||
|
||||
// Process the event and check for achievements
|
||||
switch (eventType) {
|
||||
case 'level_completed':
|
||||
// First level completion
|
||||
if (data.levelId === 1) {
|
||||
await unlockAchievement('first_steps');
|
||||
}
|
||||
|
||||
// Complete a level quickly
|
||||
if (data.timeSpent && data.timeSpent < 60) {
|
||||
await unlockAchievement('speed_demon');
|
||||
}
|
||||
|
||||
// Complete a level without hints
|
||||
if (!data.usedHint) {
|
||||
await unlockAchievement('no_hints');
|
||||
}
|
||||
|
||||
// Complete all levels
|
||||
if (data.levelId === data.allLevels) {
|
||||
await unlockAchievement('master_hacker');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'hint_used':
|
||||
// Track hint usage
|
||||
const currentLevel = gameState.currentLevel;
|
||||
if (!gameState.levelStates[currentLevel]) {
|
||||
gameState.levelStates[currentLevel] = {};
|
||||
}
|
||||
gameState.levelStates[currentLevel].usedHint = true;
|
||||
break;
|
||||
|
||||
case 'command_used':
|
||||
// Track unique commands used
|
||||
const level = gameState.currentLevel;
|
||||
if (!gameState.levelStates[level]) {
|
||||
gameState.levelStates[level] = {};
|
||||
}
|
||||
|
||||
if (!gameState.levelStates[level].uniqueCommands) {
|
||||
gameState.levelStates[level].uniqueCommands = new Set();
|
||||
}
|
||||
|
||||
if (!gameState.levelStates[level].commandCount) {
|
||||
gameState.levelStates[level].commandCount = 0;
|
||||
}
|
||||
|
||||
gameState.levelStates[level].uniqueCommands.add(data.command);
|
||||
gameState.levelStates[level].commandCount++;
|
||||
|
||||
// Check for command master achievement
|
||||
if (gameState.levelStates[level].uniqueCommands.size >= 10) {
|
||||
await unlockAchievement('command_master');
|
||||
}
|
||||
|
||||
// Check for persistence achievement
|
||||
if (gameState.levelStates[level].commandCount >= 20) {
|
||||
await unlockAchievement('persistence');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'easter_egg_found':
|
||||
await unlockAchievement('easter_egg_hunter');
|
||||
break;
|
||||
|
||||
case 'all_directories_visited':
|
||||
await unlockAchievement('explorer');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add this function to initialize achievements
|
||||
export async function initializeAchievements(): Promise<void> {
|
||||
try {
|
||||
// Check if achievements file exists, if not create it
|
||||
if (!fs.existsSync(achievementsPath)) {
|
||||
await fs.writeFile(achievementsPath, JSON.stringify(achievements, null, 2));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing achievements:', error);
|
||||
}
|
||||
}
|
@ -3,15 +3,19 @@
|
||||
import { renderMainMenu } from './ui/mainMenu';
|
||||
import { initializeGame } from './core/gameInit';
|
||||
import { registerAllLevels } from './levels';
|
||||
import { initializeAchievements } from './core/achievements';
|
||||
|
||||
async function main() {
|
||||
// Initialize game systems
|
||||
await initializeGame();
|
||||
|
||||
// Initialize achievements
|
||||
await initializeAchievements();
|
||||
|
||||
// Register all game levels
|
||||
registerAllLevels();
|
||||
|
||||
// Render the main menu to start (which now includes the boot sequence)
|
||||
// Render the main menu to start
|
||||
await renderMainMenu();
|
||||
}
|
||||
|
||||
|
87
src/ui/commandHistory.ts
Normal file
87
src/ui/commandHistory.ts
Normal file
@ -0,0 +1,87 @@
|
||||
// Maximum number of commands to store in history
|
||||
const MAX_HISTORY_SIZE = 50;
|
||||
|
||||
// Command history for each player
|
||||
const commandHistories: Record<string, string[]> = {};
|
||||
let currentHistoryIndex = -1;
|
||||
let currentInput = '';
|
||||
|
||||
// Initialize command history for a player
|
||||
export function initCommandHistory(playerName: string): void {
|
||||
if (!commandHistories[playerName]) {
|
||||
commandHistories[playerName] = [];
|
||||
}
|
||||
currentHistoryIndex = -1;
|
||||
currentInput = '';
|
||||
}
|
||||
|
||||
// Add a command to history
|
||||
export function addToHistory(playerName: string, command: string): void {
|
||||
if (!commandHistories[playerName]) {
|
||||
initCommandHistory(playerName);
|
||||
}
|
||||
|
||||
// Don't add empty commands or duplicates of the last command
|
||||
if (command.trim() === '' ||
|
||||
(commandHistories[playerName].length > 0 &&
|
||||
commandHistories[playerName][0] === command)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to the beginning of the array
|
||||
commandHistories[playerName].unshift(command);
|
||||
|
||||
// Trim history if it gets too long
|
||||
if (commandHistories[playerName].length > MAX_HISTORY_SIZE) {
|
||||
commandHistories[playerName].pop();
|
||||
}
|
||||
|
||||
// Reset index
|
||||
currentHistoryIndex = -1;
|
||||
currentInput = '';
|
||||
}
|
||||
|
||||
// Get previous command from history
|
||||
export function getPreviousCommand(playerName: string, currentCommand: string): string {
|
||||
if (!commandHistories[playerName] || commandHistories[playerName].length === 0) {
|
||||
return currentCommand;
|
||||
}
|
||||
|
||||
// Save current input if we're just starting to navigate history
|
||||
if (currentHistoryIndex === -1) {
|
||||
currentInput = currentCommand;
|
||||
}
|
||||
|
||||
// Move back in history
|
||||
currentHistoryIndex = Math.min(currentHistoryIndex + 1, commandHistories[playerName].length - 1);
|
||||
return commandHistories[playerName][currentHistoryIndex];
|
||||
}
|
||||
|
||||
// Get next command from history
|
||||
export function getNextCommand(playerName: string): string {
|
||||
if (!commandHistories[playerName] || currentHistoryIndex === -1) {
|
||||
return currentInput;
|
||||
}
|
||||
|
||||
// Move forward in history
|
||||
currentHistoryIndex = Math.max(currentHistoryIndex - 1, -1);
|
||||
|
||||
// Return original input if we've reached the end of history
|
||||
if (currentHistoryIndex === -1) {
|
||||
return currentInput;
|
||||
}
|
||||
|
||||
return commandHistories[playerName][currentHistoryIndex];
|
||||
}
|
||||
|
||||
// Get all commands in history
|
||||
export function getCommandHistory(playerName: string): string[] {
|
||||
return commandHistories[playerName] || [];
|
||||
}
|
||||
|
||||
// Clear command history
|
||||
export function clearCommandHistory(playerName: string): void {
|
||||
commandHistories[playerName] = [];
|
||||
currentHistoryIndex = -1;
|
||||
currentInput = '';
|
||||
}
|
@ -10,6 +10,8 @@ import {
|
||||
} from './visualEffects';
|
||||
import { playSound } from './soundEffects';
|
||||
import { levelUI } from './levelRenderer';
|
||||
import { addToHistory } from './commandHistory';
|
||||
import { triggerAchievement } from '../core/achievements';
|
||||
|
||||
export async function renderGameUI(): Promise<void> {
|
||||
const gameState = getCurrentGameState();
|
||||
@ -54,6 +56,10 @@ export async function renderGameUI(): Promise<void> {
|
||||
levelUI.inputBox();
|
||||
const input = await promptInput('');
|
||||
|
||||
if (input.trim()) {
|
||||
addToHistory(gameState.playerName, input);
|
||||
}
|
||||
|
||||
// Handle special commands
|
||||
if (input.startsWith('/')) {
|
||||
const command = input.slice(1).toLowerCase();
|
||||
@ -76,6 +82,7 @@ export async function renderGameUI(): Promise<void> {
|
||||
}
|
||||
|
||||
if (command === 'hint') {
|
||||
await triggerAchievement('hint_used');
|
||||
await showHint(currentLevel.hints);
|
||||
continue;
|
||||
}
|
||||
@ -93,6 +100,15 @@ export async function renderGameUI(): Promise<void> {
|
||||
if (result.completed) {
|
||||
playSound('levelComplete');
|
||||
await completeCurrentLevel();
|
||||
|
||||
// Trigger level completion achievement
|
||||
await triggerAchievement('level_completed', {
|
||||
levelId: gameState.currentLevel,
|
||||
usedHint: gameState.levelStates[gameState.currentLevel]?.usedHint || false,
|
||||
timeSpent: gameState.levelStates[gameState.currentLevel]?.timeSpent || 0,
|
||||
allLevels: getAllLevels().length
|
||||
});
|
||||
|
||||
await successAnimation('Level completed!');
|
||||
|
||||
if (result.nextAction === 'main_menu') {
|
||||
@ -116,6 +132,9 @@ export async function renderGameUI(): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When using a command, track it for achievements
|
||||
await triggerAchievement('command_used', { command: input });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,10 +14,24 @@ import {
|
||||
successAnimation,
|
||||
loadingAnimation
|
||||
} from './visualEffects';
|
||||
import { showAchievements } from '../core/achievements';
|
||||
import { renderProgressMap } from './progressMap';
|
||||
import {
|
||||
toggleSound,
|
||||
toggleAmbientSound,
|
||||
toggleSoundEffects,
|
||||
setSoundVolume,
|
||||
soundConfig,
|
||||
initSoundSystem
|
||||
} from './soundEffects';
|
||||
import { addToHistory } from './commandHistory';
|
||||
|
||||
// Track if we've shown the boot sequence
|
||||
let bootSequenceShown = false;
|
||||
|
||||
// Initialize sound system in the main menu
|
||||
initSoundSystem();
|
||||
|
||||
export async function renderMainMenu(): Promise<void> {
|
||||
// Show boot sequence only once
|
||||
if (!bootSequenceShown) {
|
||||
@ -41,8 +55,10 @@ export async function renderMainMenu(): Promise<void> {
|
||||
'1. ' + theme.accent('New Game'),
|
||||
'2. ' + theme.accent('Load Game'),
|
||||
'3. ' + theme.accent('Leaderboard'),
|
||||
'4. ' + theme.accent('Settings'),
|
||||
'5. ' + theme.accent('Exit')
|
||||
'4. ' + theme.accent('Achievements'),
|
||||
'5. ' + theme.accent('Progress Map'),
|
||||
'6. ' + theme.accent('Settings'),
|
||||
'7. ' + theme.accent('Exit')
|
||||
];
|
||||
|
||||
console.log(drawBox('MAIN MENU', menuOptions.join('\n')));
|
||||
@ -61,9 +77,18 @@ export async function renderMainMenu(): Promise<void> {
|
||||
await showLeaderboard();
|
||||
break;
|
||||
case '4':
|
||||
await showSettings();
|
||||
await showAchievements();
|
||||
await promptInput('Press Enter to continue...');
|
||||
break;
|
||||
case '5':
|
||||
clearScreen();
|
||||
renderProgressMap();
|
||||
await promptInput('Press Enter to continue...');
|
||||
break;
|
||||
case '6':
|
||||
await showSettings();
|
||||
break;
|
||||
case '7':
|
||||
await animateText('Thanks for playing Terminal Escape!', 30);
|
||||
process.exit(0);
|
||||
default:
|
||||
@ -204,4 +229,69 @@ async function showLeaderboard(): Promise<void> {
|
||||
|
||||
console.log('');
|
||||
await promptInput('Press Enter to return to main menu...');
|
||||
}
|
||||
|
||||
// Add this function to the mainMenu.ts file
|
||||
async function soundSettings(): Promise<void> {
|
||||
const theme = getTheme();
|
||||
|
||||
while (true) {
|
||||
clearScreen();
|
||||
console.log(theme.accent('=== SOUND SETTINGS ==='));
|
||||
console.log('');
|
||||
|
||||
console.log(`1. Sound: ${soundConfig.enabled ? theme.success('ON') : theme.error('OFF')}`);
|
||||
console.log(`2. Ambient Sound: ${soundConfig.ambientEnabled ? theme.success('ON') : theme.error('OFF')}`);
|
||||
console.log(`3. Sound Effects: ${soundConfig.effectsEnabled ? theme.success('ON') : theme.error('OFF')}`);
|
||||
console.log(`4. Volume: ${Math.round(soundConfig.volume * 100)}%`);
|
||||
console.log('5. Back to Settings');
|
||||
console.log('');
|
||||
|
||||
const choice = await promptInput('Select an option: ');
|
||||
|
||||
switch (choice) {
|
||||
case '1':
|
||||
toggleSound();
|
||||
break;
|
||||
case '2':
|
||||
toggleAmbientSound();
|
||||
break;
|
||||
case '3':
|
||||
toggleSoundEffects();
|
||||
break;
|
||||
case '4':
|
||||
await changeVolume();
|
||||
break;
|
||||
case '5':
|
||||
return;
|
||||
default:
|
||||
console.log(theme.error('Invalid option. Press Enter to continue...'));
|
||||
await promptInput('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add this function to change volume
|
||||
async function changeVolume(): Promise<void> {
|
||||
const theme = getTheme();
|
||||
|
||||
clearScreen();
|
||||
console.log(theme.accent('=== VOLUME SETTINGS ==='));
|
||||
console.log('');
|
||||
|
||||
console.log('Current volume: ' + Math.round(soundConfig.volume * 100) + '%');
|
||||
console.log('Enter a value between 0 and 100:');
|
||||
|
||||
const input = await promptInput('');
|
||||
const volume = parseInt(input);
|
||||
|
||||
if (isNaN(volume) || volume < 0 || volume > 100) {
|
||||
console.log(theme.error('Invalid volume. Please enter a number between 0 and 100.'));
|
||||
await promptInput('Press Enter to continue...');
|
||||
return;
|
||||
}
|
||||
|
||||
setSoundVolume(volume / 100);
|
||||
console.log(theme.success(`Volume set to ${volume}%`));
|
||||
await promptInput('Press Enter to continue...');
|
||||
}
|
73
src/ui/progressMap.ts
Normal file
73
src/ui/progressMap.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { getAllLevels, getLevelById } from '../core/levelSystem';
|
||||
import { getCurrentGameState } from '../core/gameState';
|
||||
import { getTheme } from './visualEffects';
|
||||
|
||||
export function renderProgressMap(): void {
|
||||
const gameState = getCurrentGameState();
|
||||
if (!gameState) return;
|
||||
|
||||
const theme = getTheme();
|
||||
const allLevels = getAllLevels();
|
||||
const currentLevelId = gameState.currentLevel;
|
||||
|
||||
console.log(theme.accent('=== Progress Map ==='));
|
||||
console.log('');
|
||||
|
||||
// Calculate the maximum level name length for formatting
|
||||
const maxNameLength = Math.max(...allLevels.map(level => level.name.length));
|
||||
|
||||
// Create a visual map of levels
|
||||
console.log('┌' + '─'.repeat(maxNameLength + 22) + '┐');
|
||||
|
||||
allLevels.forEach((level, index) => {
|
||||
const levelNumber = level.id;
|
||||
const isCurrentLevel = levelNumber === currentLevelId;
|
||||
const isCompleted = levelNumber < currentLevelId;
|
||||
const isLocked = levelNumber > currentLevelId;
|
||||
|
||||
let statusIcon;
|
||||
let levelName;
|
||||
|
||||
if (isCompleted) {
|
||||
statusIcon = theme.success('✓');
|
||||
levelName = theme.success(level.name.padEnd(maxNameLength));
|
||||
} else if (isCurrentLevel) {
|
||||
statusIcon = theme.accent('▶');
|
||||
levelName = theme.accent(level.name.padEnd(maxNameLength));
|
||||
} else if (isLocked) {
|
||||
statusIcon = theme.secondary('🔒');
|
||||
levelName = theme.secondary(level.name.padEnd(maxNameLength));
|
||||
}
|
||||
|
||||
console.log(`│ ${statusIcon} Level ${levelNumber.toString().padStart(2)} │ ${levelName} │`);
|
||||
|
||||
// Add connector line between levels
|
||||
if (index < allLevels.length - 1) {
|
||||
console.log('│ ' + ' '.repeat(maxNameLength + 20) + '│');
|
||||
console.log('│ ' + theme.secondary('│').padStart(7) + ' '.repeat(maxNameLength + 14) + '│');
|
||||
console.log('│ ' + theme.secondary('▼').padStart(7) + ' '.repeat(maxNameLength + 14) + '│');
|
||||
console.log('│ ' + ' '.repeat(maxNameLength + 20) + '│');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('└' + '─'.repeat(maxNameLength + 22) + '┘');
|
||||
|
||||
// Show completion percentage
|
||||
const completedLevels = Math.max(0, currentLevelId - 1);
|
||||
const completionPercentage = Math.round((completedLevels / allLevels.length) * 100);
|
||||
|
||||
console.log('');
|
||||
console.log(`Overall Progress: ${completedLevels}/${allLevels.length} levels completed (${completionPercentage}%)`);
|
||||
|
||||
// Visual progress bar
|
||||
const progressBarWidth = 40;
|
||||
const filledWidth = Math.round((completionPercentage / 100) * progressBarWidth);
|
||||
const emptyWidth = progressBarWidth - filledWidth;
|
||||
|
||||
const progressBar = '[' +
|
||||
theme.success('='.repeat(filledWidth)) +
|
||||
theme.secondary('-'.repeat(emptyWidth)) +
|
||||
'] ' + completionPercentage + '%';
|
||||
|
||||
console.log(progressBar);
|
||||
}
|
@ -1,20 +1,49 @@
|
||||
import player from 'play-sound';
|
||||
// Simplified sound effects module that doesn't actually play sounds
|
||||
// but maintains the interface for the rest of the application
|
||||
|
||||
const audioPlayer = player({});
|
||||
export const soundConfig = {
|
||||
enabled: false,
|
||||
volume: 0.5,
|
||||
ambientEnabled: false,
|
||||
effectsEnabled: false
|
||||
};
|
||||
|
||||
// Play a sound effect (does nothing)
|
||||
export function playSound(sound: 'success' | 'error' | 'typing' | 'levelComplete'): void {
|
||||
try {
|
||||
const soundMap = {
|
||||
success: 'sounds/success.wav',
|
||||
error: 'sounds/error.wav',
|
||||
typing: 'sounds/typing.wav',
|
||||
levelComplete: 'sounds/level-complete.wav'
|
||||
};
|
||||
|
||||
audioPlayer.play(soundMap[sound], (err) => {
|
||||
if (err) console.error('Error playing sound:', err);
|
||||
});
|
||||
} catch (error) {
|
||||
// Silently fail if sound can't be played
|
||||
}
|
||||
// No-op function to maintain API compatibility
|
||||
}
|
||||
|
||||
// Play ambient sound (does nothing)
|
||||
export function playAmbientSound(): void {
|
||||
// No-op function to maintain API compatibility
|
||||
}
|
||||
|
||||
// Stop the ambient sound (does nothing)
|
||||
export function stopAmbientSound(): void {
|
||||
// No-op function to maintain API compatibility
|
||||
}
|
||||
|
||||
// Toggle sound on/off
|
||||
export function toggleSound(): boolean {
|
||||
return soundConfig.enabled;
|
||||
}
|
||||
|
||||
// Toggle ambient sound on/off
|
||||
export function toggleAmbientSound(): boolean {
|
||||
return soundConfig.ambientEnabled;
|
||||
}
|
||||
|
||||
// Toggle sound effects on/off
|
||||
export function toggleSoundEffects(): boolean {
|
||||
return soundConfig.effectsEnabled;
|
||||
}
|
||||
|
||||
// Set sound volume
|
||||
export function setSoundVolume(volume: number): void {
|
||||
soundConfig.volume = Math.max(0, Math.min(1, volume));
|
||||
}
|
||||
|
||||
// Initialize sound system (does nothing)
|
||||
export function initSoundSystem(): void {
|
||||
// No-op function to maintain API compatibility
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import readline from 'readline';
|
||||
import kleur from 'kleur';
|
||||
import { getCurrentGameState } from '../core/gameState';
|
||||
import { getPreviousCommand, getNextCommand } from './commandHistory';
|
||||
|
||||
// Enable colors
|
||||
kleur.enabled = true;
|
||||
|
Loading…
x
Reference in New Issue
Block a user