mirror of
https://github.com/tcsenpai/shellquest.git
synced 2025-06-02 09:20:17 +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"
|
"bun-types": "latest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"beep": "^0.0.0",
|
||||||
"figlet": "^1.6.0",
|
"figlet": "^1.6.0",
|
||||||
"figlet-cli": "^0.2.0",
|
"figlet-cli": "^0.2.0",
|
||||||
"gradient-string": "^2.0.2",
|
"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 { renderMainMenu } from './ui/mainMenu';
|
||||||
import { initializeGame } from './core/gameInit';
|
import { initializeGame } from './core/gameInit';
|
||||||
import { registerAllLevels } from './levels';
|
import { registerAllLevels } from './levels';
|
||||||
|
import { initializeAchievements } from './core/achievements';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Initialize game systems
|
// Initialize game systems
|
||||||
await initializeGame();
|
await initializeGame();
|
||||||
|
|
||||||
|
// Initialize achievements
|
||||||
|
await initializeAchievements();
|
||||||
|
|
||||||
// Register all game levels
|
// Register all game levels
|
||||||
registerAllLevels();
|
registerAllLevels();
|
||||||
|
|
||||||
// Render the main menu to start (which now includes the boot sequence)
|
// Render the main menu to start
|
||||||
await renderMainMenu();
|
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';
|
} from './visualEffects';
|
||||||
import { playSound } from './soundEffects';
|
import { playSound } from './soundEffects';
|
||||||
import { levelUI } from './levelRenderer';
|
import { levelUI } from './levelRenderer';
|
||||||
|
import { addToHistory } from './commandHistory';
|
||||||
|
import { triggerAchievement } from '../core/achievements';
|
||||||
|
|
||||||
export async function renderGameUI(): Promise<void> {
|
export async function renderGameUI(): Promise<void> {
|
||||||
const gameState = getCurrentGameState();
|
const gameState = getCurrentGameState();
|
||||||
@ -54,6 +56,10 @@ export async function renderGameUI(): Promise<void> {
|
|||||||
levelUI.inputBox();
|
levelUI.inputBox();
|
||||||
const input = await promptInput('');
|
const input = await promptInput('');
|
||||||
|
|
||||||
|
if (input.trim()) {
|
||||||
|
addToHistory(gameState.playerName, input);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle special commands
|
// Handle special commands
|
||||||
if (input.startsWith('/')) {
|
if (input.startsWith('/')) {
|
||||||
const command = input.slice(1).toLowerCase();
|
const command = input.slice(1).toLowerCase();
|
||||||
@ -76,6 +82,7 @@ export async function renderGameUI(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (command === 'hint') {
|
if (command === 'hint') {
|
||||||
|
await triggerAchievement('hint_used');
|
||||||
await showHint(currentLevel.hints);
|
await showHint(currentLevel.hints);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -93,6 +100,15 @@ export async function renderGameUI(): Promise<void> {
|
|||||||
if (result.completed) {
|
if (result.completed) {
|
||||||
playSound('levelComplete');
|
playSound('levelComplete');
|
||||||
await completeCurrentLevel();
|
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!');
|
await successAnimation('Level completed!');
|
||||||
|
|
||||||
if (result.nextAction === 'main_menu') {
|
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,
|
successAnimation,
|
||||||
loadingAnimation
|
loadingAnimation
|
||||||
} from './visualEffects';
|
} 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
|
// Track if we've shown the boot sequence
|
||||||
let bootSequenceShown = false;
|
let bootSequenceShown = false;
|
||||||
|
|
||||||
|
// Initialize sound system in the main menu
|
||||||
|
initSoundSystem();
|
||||||
|
|
||||||
export async function renderMainMenu(): Promise<void> {
|
export async function renderMainMenu(): Promise<void> {
|
||||||
// Show boot sequence only once
|
// Show boot sequence only once
|
||||||
if (!bootSequenceShown) {
|
if (!bootSequenceShown) {
|
||||||
@ -41,8 +55,10 @@ export async function renderMainMenu(): Promise<void> {
|
|||||||
'1. ' + theme.accent('New Game'),
|
'1. ' + theme.accent('New Game'),
|
||||||
'2. ' + theme.accent('Load Game'),
|
'2. ' + theme.accent('Load Game'),
|
||||||
'3. ' + theme.accent('Leaderboard'),
|
'3. ' + theme.accent('Leaderboard'),
|
||||||
'4. ' + theme.accent('Settings'),
|
'4. ' + theme.accent('Achievements'),
|
||||||
'5. ' + theme.accent('Exit')
|
'5. ' + theme.accent('Progress Map'),
|
||||||
|
'6. ' + theme.accent('Settings'),
|
||||||
|
'7. ' + theme.accent('Exit')
|
||||||
];
|
];
|
||||||
|
|
||||||
console.log(drawBox('MAIN MENU', menuOptions.join('\n')));
|
console.log(drawBox('MAIN MENU', menuOptions.join('\n')));
|
||||||
@ -61,9 +77,18 @@ export async function renderMainMenu(): Promise<void> {
|
|||||||
await showLeaderboard();
|
await showLeaderboard();
|
||||||
break;
|
break;
|
||||||
case '4':
|
case '4':
|
||||||
await showSettings();
|
await showAchievements();
|
||||||
|
await promptInput('Press Enter to continue...');
|
||||||
break;
|
break;
|
||||||
case '5':
|
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);
|
await animateText('Thanks for playing Terminal Escape!', 30);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
default:
|
default:
|
||||||
@ -204,4 +229,69 @@ async function showLeaderboard(): Promise<void> {
|
|||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
await promptInput('Press Enter to return to main menu...');
|
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 {
|
export function playSound(sound: 'success' | 'error' | 'typing' | 'levelComplete'): void {
|
||||||
try {
|
// No-op function to maintain API compatibility
|
||||||
const soundMap = {
|
}
|
||||||
success: 'sounds/success.wav',
|
|
||||||
error: 'sounds/error.wav',
|
// Play ambient sound (does nothing)
|
||||||
typing: 'sounds/typing.wav',
|
export function playAmbientSound(): void {
|
||||||
levelComplete: 'sounds/level-complete.wav'
|
// No-op function to maintain API compatibility
|
||||||
};
|
}
|
||||||
|
|
||||||
audioPlayer.play(soundMap[sound], (err) => {
|
// Stop the ambient sound (does nothing)
|
||||||
if (err) console.error('Error playing sound:', err);
|
export function stopAmbientSound(): void {
|
||||||
});
|
// No-op function to maintain API compatibility
|
||||||
} catch (error) {
|
}
|
||||||
// Silently fail if sound can't be played
|
|
||||||
}
|
// 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 readline from 'readline';
|
||||||
import kleur from 'kleur';
|
import kleur from 'kleur';
|
||||||
|
import { getCurrentGameState } from '../core/gameState';
|
||||||
|
import { getPreviousCommand, getNextCommand } from './commandHistory';
|
||||||
|
|
||||||
// Enable colors
|
// Enable colors
|
||||||
kleur.enabled = true;
|
kleur.enabled = true;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user