This commit is contained in:
tcsenpai 2025-04-01 12:03:30 +02:00
parent 1ad0f8d871
commit 5c9be8fc79
8 changed files with 402 additions and 156 deletions

View File

@ -3,6 +3,8 @@ import path from 'path';
import { getCurrentGameState } from './gameState'; import { getCurrentGameState } from './gameState';
import { getTheme, successAnimation } from '../ui/visualEffects'; import { getTheme, successAnimation } from '../ui/visualEffects';
import { playSound } from '../ui/soundEffects'; import { playSound } from '../ui/soundEffects';
import { getCurrentProfile } from './playerProfile';
import { saveProfile } from './playerProfile';
// Define achievement types // Define achievement types
export interface Achievement { export interface Achievement {
@ -81,14 +83,14 @@ const achievementsPath = path.join(process.cwd(), 'achievements.json');
// Load achievements from file // Load achievements from file
export async function loadAchievements(): Promise<Achievement[]> { export async function loadAchievements(): Promise<Achievement[]> {
try { const profile = await getCurrentProfile();
const data = await fs.readFile(achievementsPath, 'utf-8');
return JSON.parse(data); if (profile && profile.achievements.length > 0) {
} catch (error) { return profile.achievements;
// If file doesn't exist, create it with default achievements
await saveAchievements(achievements);
return achievements;
} }
// Return default achievements if no profile exists or no achievements in profile
return [...achievements];
} }
// Save achievements to file // Save achievements to file
@ -109,36 +111,42 @@ export async function getPlayerAchievements(playerName: string): Promise<Achieve
} }
// Unlock an achievement // Unlock an achievement
export async function unlockAchievement(achievementId: string): Promise<boolean> { export async function unlockAchievement(id: string): Promise<boolean> {
const gameState = getCurrentGameState(); const profile = await getCurrentProfile();
if (!gameState) return false; if (!profile) return false;
// Load achievements // Find the achievement in the profile
const allAchievements = await loadAchievements(); let achievementIndex = profile.achievements.findIndex(a => a.id === id);
const achievement = allAchievements.find(a => a.id === achievementId);
if (!achievement || achievement.unlocked) { // If not found in profile, check the default achievements
return false; // Achievement doesn't exist or is already unlocked if (achievementIndex === -1) {
const defaultAchievement = achievements.find(a => a.id === id);
if (!defaultAchievement) return false;
// Add the achievement to the profile
profile.achievements.push({
...defaultAchievement,
unlocked: true,
unlockedAt: Date.now()
});
} else {
// Achievement already exists in profile, just update it
if (profile.achievements[achievementIndex].unlocked) {
return false; // Already unlocked
}
profile.achievements[achievementIndex].unlocked = true;
profile.achievements[achievementIndex].unlockedAt = Date.now();
} }
// Unlock the achievement // Save the updated profile
achievement.unlocked = true; await saveProfile(profile);
achievement.unlockedAt = Date.now();
// Save achievements
await saveAchievements(allAchievements);
// Show achievement notification // Show achievement notification
const theme = getTheme(); const achievement = profile.achievements.find(a => a.id === id);
console.log('\n'); if (achievement) {
console.log('╔' + '═'.repeat(50) + '╗'); await showAchievementNotification(achievement);
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; return true;
} }

View File

@ -33,15 +33,37 @@ export async function initializeGame() {
} }
// Helper functions for save management // Helper functions for save management
export async function listSaves() { export async function listSaves(): Promise<string[]> {
try { try {
await ensureSavesDir();
const files = await fs.readdir(SAVE_DIR); const files = await fs.readdir(SAVE_DIR);
return files.filter(file => file.endsWith('.json'));
// Filter out profile files and remove .json extension
return files
.filter(file => file.endsWith('.json') && !file.endsWith('_profile.json'))
.map(file => file.replace('.json', ''));
} catch (error) { } catch (error) {
console.error('Failed to list saves:', error); console.error('Error listing saves:', error);
return []; return [];
} }
} }
export const getSavePath = (saveName: string) => path.join(SAVE_DIR, `${saveName}.json`); export function getSavePath(saveName: string): string {
export const getLeaderboardPath = () => LEADERBOARD_PATH; // Remove .json extension if it's already there
const baseName = saveName.endsWith('.json')
? saveName.slice(0, -5)
: saveName;
return path.join(SAVE_DIR, `${baseName}.json`);
}
export const getLeaderboardPath = () => LEADERBOARD_PATH;
// Ensure saves directory exists
export async function ensureSavesDir(): Promise<void> {
try {
await fs.mkdir(SAVE_DIR, { recursive: true });
} catch (error) {
console.error('Error creating saves directory:', error);
}
}

View File

@ -1,5 +1,7 @@
import fs from 'fs/promises'; import fs from 'fs/promises';
import { getSavePath } from './gameInit'; import { getSavePath } from './gameInit';
import { createProfile, loadProfile, saveProfile } from './playerProfile';
import { getCurrentProfile } from './playerProfile';
export interface GameState { export interface GameState {
playerName: string; playerName: string;
@ -17,8 +19,12 @@ export function getCurrentGameState(): GameState | null {
return currentGameState; return currentGameState;
} }
export function setCurrentGameState(gameState: GameState): void {
currentGameState = gameState;
}
export function createNewGame(playerName: string): GameState { export function createNewGame(playerName: string): GameState {
const newState: GameState = { const gameState: GameState = {
playerName, playerName,
currentLevel: 1, currentLevel: 1,
startTime: Date.now(), startTime: Date.now(),
@ -28,8 +34,15 @@ export function createNewGame(playerName: string): GameState {
levelStates: {} levelStates: {}
}; };
currentGameState = newState; // Create a new profile for this player
return newState; createOrLoadProfile(playerName);
setCurrentGameState(gameState);
// Save the initial game state
saveGame(playerName);
return gameState;
} }
export async function saveGame(saveName?: string): Promise<boolean> { export async function saveGame(saveName?: string): Promise<boolean> {
@ -61,6 +74,10 @@ export async function loadGame(saveName: string): Promise<boolean> {
try { try {
const saveData = await fs.readFile(getSavePath(saveName), 'utf-8'); const saveData = await fs.readFile(getSavePath(saveName), 'utf-8');
currentGameState = JSON.parse(saveData) as GameState; currentGameState = JSON.parse(saveData) as GameState;
// Make sure the profile exists
await createOrLoadProfile(currentGameState.playerName);
console.log(`Game loaded: ${saveName}`); console.log(`Game loaded: ${saveName}`);
return true; return true;
} catch (error) { } catch (error) {
@ -72,4 +89,36 @@ export async function loadGame(saveName: string): Promise<boolean> {
export async function autoSave(): Promise<boolean> { export async function autoSave(): Promise<boolean> {
if (!currentGameState) return false; if (!currentGameState) return false;
return saveGame(`${currentGameState.playerName}_autosave`); return saveGame(`${currentGameState.playerName}_autosave`);
}
async function createOrLoadProfile(playerName: string): Promise<void> {
const profile = await loadProfile(playerName);
if (!profile) {
await createProfile(playerName);
}
}
export async function completeCurrentLevel(): Promise<void> {
const gameState = getCurrentGameState();
if (!gameState) return;
// Add the level to completed levels in the game state
if (!gameState.completedLevels.includes(gameState.currentLevel)) {
gameState.completedLevels.push(gameState.currentLevel);
}
// Also update the profile
const profile = await getCurrentProfile();
if (profile) {
if (!profile.completedLevels.includes(gameState.currentLevel)) {
profile.completedLevels.push(gameState.currentLevel);
await saveProfile(profile);
}
}
// Move to the next level
gameState.currentLevel++;
// Save the game
await saveGame();
} }

88
src/core/playerProfile.ts Normal file
View File

@ -0,0 +1,88 @@
import fs from 'fs/promises';
import path from 'path';
import { Achievement } from './achievements';
import { getCurrentGameState } from './gameState';
import { getSavePath } from './gameInit';
export interface PlayerProfile {
playerName: string;
achievements: Achievement[];
lastPlayed: number;
totalPlayTime: number;
completedLevels: number[];
}
// Use the same directory for profiles and saves
const profilesDir = path.join(process.cwd(), 'saves');
// Ensure profiles directory exists
export async function ensureProfilesDir(): Promise<void> {
try {
await fs.mkdir(profilesDir, { recursive: true });
} catch (error) {
console.error('Error creating profiles directory:', error);
}
}
// Get profile path for a player
function getProfilePath(playerName: string): string {
return path.join(profilesDir, `${playerName.toLowerCase()}_profile.json`);
}
// Create a new player profile
export async function createProfile(playerName: string): Promise<PlayerProfile> {
const profile: PlayerProfile = {
playerName,
achievements: [],
lastPlayed: Date.now(),
totalPlayTime: 0,
completedLevels: []
};
await saveProfile(profile);
return profile;
}
// Load a player profile
export async function loadProfile(playerName: string): Promise<PlayerProfile | null> {
try {
const profilePath = getProfilePath(playerName);
const data = await fs.readFile(profilePath, 'utf-8');
return JSON.parse(data) as PlayerProfile;
} catch (error) {
// Profile doesn't exist or can't be read
return null;
}
}
// Save a player profile
export async function saveProfile(profile: PlayerProfile): Promise<void> {
try {
const profilePath = getProfilePath(profile.playerName);
await fs.writeFile(profilePath, JSON.stringify(profile, null, 2));
} catch (error) {
console.error('Error saving profile:', error);
}
}
// Get current player profile
export async function getCurrentProfile(): Promise<PlayerProfile | null> {
const gameState = getCurrentGameState();
if (!gameState) return null;
const profile = await loadProfile(gameState.playerName);
return profile;
}
// List all profiles
export async function listProfiles(): Promise<string[]> {
try {
const files = await fs.readdir(profilesDir);
return files
.filter(file => file.endsWith('_profile.json'))
.map(file => file.replace('_profile.json', ''));
} catch (error) {
console.error('Error listing profiles:', error);
return [];
}
}

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { renderMainMenu } from './ui/mainMenu'; import { renderEntryMenu } from './ui/entryMenu';
import { initializeGame } from './core/gameInit'; import { initializeGame } from './core/gameInit';
import { registerAllLevels } from './levels'; import { registerAllLevels } from './levels';
import { initializeAchievements } from './core/achievements'; import { initializeAchievements } from './core/achievements';
@ -15,8 +15,8 @@ async function main() {
// Register all game levels // Register all game levels
registerAllLevels(); registerAllLevels();
// Render the main menu to start // Render the entry menu to start
await renderMainMenu(); await renderEntryMenu();
} }
main().catch(error => { main().catch(error => {

131
src/ui/entryMenu.ts Normal file
View File

@ -0,0 +1,131 @@
import { clearScreen, promptInput, drawBox } from './uiHelpers';
import { generateLogo, getTheme, bootSequence } from './visualEffects';
import { loadProfile, createProfile, listProfiles } from '../core/playerProfile';
import { createNewGame, loadGame } from '../core/gameState';
import { renderMainMenu } from './mainMenu';
import { successAnimation, loadingAnimation } from './visualEffects';
import { listSaves } from '../core/gameInit';
// Track if we've shown the boot sequence
let bootSequenceShown = false;
export async function renderEntryMenu(): Promise<void> {
// Show boot sequence only once
if (!bootSequenceShown) {
await bootSequence();
bootSequenceShown = true;
} else {
clearScreen();
console.log(generateLogo());
console.log('');
}
const theme = getTheme();
while (true) {
clearScreen();
console.log(generateLogo());
console.log(theme.secondary('A Linux Terminal Escape Room Game'));
console.log('');
const menuOptions = [
'1. ' + theme.accent('New Game'),
'2. ' + theme.accent('Load Game'),
'3. ' + theme.accent('Exit')
];
console.log(drawBox('WELCOME', menuOptions.join('\n')));
console.log('');
const choice = await promptInput('Select an option: ');
if (choice === '1') {
const success = await newGameMenu();
if (success) {
await renderMainMenu();
return;
}
} else if (choice === '2') {
const success = await loadGameMenu();
if (success) {
await renderMainMenu();
return;
}
} else if (choice === '3') {
console.log('Thanks for playing Terminal Escape!');
process.exit(0);
} else {
console.log(theme.error('Invalid option. Press Enter to continue...'));
await promptInput('');
}
}
}
async function newGameMenu(): Promise<boolean> {
const theme = getTheme();
clearScreen();
console.log(theme.accent('=== NEW GAME ==='));
console.log('');
const playerName = await promptInput('Enter your name: ');
if (!playerName) {
console.log(theme.error('Name cannot be empty.'));
await promptInput('Press Enter to continue...');
return false;
}
await loadingAnimation('Creating new game...', 1000);
// Create a new game state
const gameState = createNewGame(playerName);
await successAnimation('Game created successfully!');
return true;
}
async function loadGameMenu(): Promise<boolean> {
const theme = getTheme();
clearScreen();
console.log(theme.accent('=== LOAD GAME ==='));
console.log('');
// Get list of save files
const saveFiles = await listSaves();
if (saveFiles.length === 0) {
console.log(theme.warning('No saved games found.'));
await promptInput('Press Enter to continue...');
return false;
}
console.log('Available saved games:');
saveFiles.forEach((save, index) => {
console.log(`${index + 1}. ${theme.accent(save)}`);
});
console.log('');
const choice = await promptInput('Select a saved game (or 0 to cancel): ');
const choiceNum = parseInt(choice);
if (choiceNum === 0 || isNaN(choiceNum) || choiceNum > saveFiles.length) {
return false;
}
const saveName = saveFiles[choiceNum - 1];
// Try to load the save
await loadingAnimation('Loading game...', 1000);
const success = await loadGame(saveName);
if (success) {
await successAnimation('Game loaded successfully!');
return true;
} else {
console.log(theme.error('Failed to load game.'));
await promptInput('Press Enter to continue...');
return false;
}
}

View File

@ -1,4 +1,4 @@
import { createNewGame, loadGame } from '../core/gameState'; import { createNewGame, loadGame, getCurrentGameState } from '../core/gameState';
import { listSaves } from '../core/gameInit'; import { listSaves } from '../core/gameInit';
import { getLeaderboard, formatTime } from '../core/leaderboard'; import { getLeaderboard, formatTime } from '../core/leaderboard';
import { startLevel, getAllLevels } from '../core/levelSystem'; import { startLevel, getAllLevels } from '../core/levelSystem';
@ -25,6 +25,7 @@ import {
initSoundSystem initSoundSystem
} from './soundEffects'; } from './soundEffects';
import { addToHistory } from './commandHistory'; import { addToHistory } from './commandHistory';
import { renderEntryMenu } from './entryMenu';
// Track if we've shown the boot sequence // Track if we've shown the boot sequence
let bootSequenceShown = false; let bootSequenceShown = false;
@ -33,14 +34,11 @@ let bootSequenceShown = false;
initSoundSystem(); initSoundSystem();
export async function renderMainMenu(): Promise<void> { export async function renderMainMenu(): Promise<void> {
// Show boot sequence only once const gameState = getCurrentGameState();
if (!bootSequenceShown) { if (!gameState) {
await bootSequence(); // If no game state, go back to entry menu
bootSequenceShown = true; await renderEntryMenu();
} else { return;
clearScreen();
console.log(generateLogo());
console.log('');
} }
const theme = getTheme(); const theme = getTheme();
@ -52,48 +50,52 @@ export async function renderMainMenu(): Promise<void> {
console.log(''); console.log('');
const menuOptions = [ const menuOptions = [
'1. ' + theme.accent('New Game'), '1. ' + theme.accent('Continue Game'),
'2. ' + theme.accent('Load Game'), '2. ' + theme.accent('Achievements'),
'3. ' + theme.accent('Leaderboard'), '3. ' + theme.accent('Progress Map'),
'4. ' + theme.accent('Achievements'), '4. ' + theme.accent('Leaderboard'),
'5. ' + theme.accent('Progress Map'), '5. ' + theme.accent('Settings'),
'6. ' + theme.accent('Settings'), '6. ' + theme.accent('Back to Entry Menu'),
'7. ' + theme.accent('Exit') '7. ' + theme.accent('Exit')
]; ];
console.log(drawBox('MAIN MENU', menuOptions.join('\n'))); console.log(drawBox('MAIN MENU', menuOptions.join('\n')));
console.log(''); console.log('');
console.log(theme.info(`Player: ${gameState.playerName}`));
console.log('');
const choice = await promptInput('Select an option: '); const choice = await promptInput('Select an option: ');
switch (choice) { if (choice === '1') {
case '1': // Continue game
await newGameMenu(); startLevel(gameState.currentLevel);
break; await renderGameUI();
case '2': } else if (choice === '2') {
await loadGameMenu(); // Show achievements
break; await showAchievements();
case '3': await promptInput('Press Enter to continue...');
await showLeaderboard(); } else if (choice === '3') {
break; // Show progress map
case '4': clearScreen();
await showAchievements(); await renderProgressMap();
await promptInput('Press Enter to continue...'); await promptInput('Press Enter to continue...');
break; } else if (choice === '4') {
case '5': // Show leaderboard
clearScreen(); await showLeaderboard();
renderProgressMap(); } else if (choice === '5') {
await promptInput('Press Enter to continue...'); // Settings
break; await showSettings();
case '6': } else if (choice === '6') {
await showSettings(); // Back to entry menu
break; await renderEntryMenu();
case '7': return;
await animateText('Thanks for playing Terminal Escape!', 30); } else if (choice === '7') {
process.exit(0); // Exit
default: await animateText('Thanks for playing Terminal Escape!', 30);
console.log(theme.error('Invalid option. Press Enter to continue...')); process.exit(0);
await promptInput(''); } else {
console.log(theme.error('Invalid option. Press Enter to continue...'));
await promptInput('');
} }
} }
} }
@ -150,80 +152,22 @@ async function changeTheme(): Promise<void> {
} }
} }
// Update the existing functions to use the current theme async function showLeaderboard(): Promise<void> {
async function newGameMenu(): Promise<void> {
const theme = getTheme(); const theme = getTheme();
clearScreen(); clearScreen();
console.log(theme.accent('=== NEW GAME ===')); console.log(theme.accent('=== LEADERBOARD ==='));
console.log('');
const playerName = await promptInput('Enter your name: ');
if (!playerName) {
console.log(theme.error('Name cannot be empty.'));
await promptInput('Press Enter to continue...');
return;
}
await loadingAnimation('Creating new game...', 1000);
const gameState = createNewGame(playerName);
startLevel(gameState.currentLevel);
await renderGameUI();
}
async function loadGameMenu(): Promise<void> {
clearScreen();
console.log('=== Load Game ===');
console.log('');
const saves = await listSaves();
if (saves.length === 0) {
console.log('No saved games found. Press Enter to return to main menu...');
await promptInput('');
return;
}
console.log('Available saves:');
saves.forEach((save, index) => {
console.log(`${index + 1}. ${save.replace('.json', '')}`);
});
console.log('');
const choice = await promptInput('Select a save to load (or 0 to cancel): ');
const choiceNum = parseInt(choice);
if (choiceNum === 0 || isNaN(choiceNum) || choiceNum > saves.length) {
return;
}
const saveName = saves[choiceNum - 1].replace('.json', '');
const success = await loadGame(saveName);
if (success) {
await renderGameUI();
} else {
console.log('Failed to load game. Press Enter to return to main menu...');
await promptInput('');
}
}
async function showLeaderboard(): Promise<void> {
clearScreen();
console.log('=== Leaderboard ===');
console.log(''); console.log('');
const leaderboard = await getLeaderboard(); const leaderboard = await getLeaderboard();
if (leaderboard.players.length === 0) { if (leaderboard.players.length === 0) {
console.log('No entries yet. Be the first to complete the game!'); console.log(theme.warning('No entries yet. Be the first to complete the game!'));
} else { } else {
console.log('Top Players:'); console.log('Top Players:');
console.log('-----------'); console.log('-----------');
leaderboard.players.slice(0, 10).forEach((entry, index) => { leaderboard.players.slice(0, 10).forEach((entry, index) => {
console.log(`${index + 1}. ${entry.playerName} - ${formatTime(entry.completionTime)}`); console.log(`${index + 1}. ${theme.accent(entry.playerName)} - ${formatTime(entry.completionTime)}`);
}); });
} }

View File

@ -1,14 +1,18 @@
import { getAllLevels, getLevelById } from '../core/levelSystem'; import { getAllLevels } from '../core/levelSystem';
import { getCurrentGameState } from '../core/gameState'; import { getCurrentProfile } from '../core/playerProfile';
import { getTheme } from './visualEffects'; import { getTheme } from './visualEffects';
export function renderProgressMap(): void { export async function renderProgressMap(): Promise<void> {
const gameState = getCurrentGameState(); const profile = await getCurrentProfile();
if (!gameState) return; if (!profile) {
console.log('No active player profile. Please start a game first.');
return;
}
const theme = getTheme(); const theme = getTheme();
const allLevels = getAllLevels(); const allLevels = getAllLevels();
const currentLevelId = gameState.currentLevel; const completedLevels = profile.completedLevels;
const currentLevelId = Math.max(...completedLevels) + 1;
console.log(theme.accent('=== Progress Map ===')); console.log(theme.accent('=== Progress Map ==='));
console.log(''); console.log('');
@ -53,11 +57,11 @@ export function renderProgressMap(): void {
console.log('└' + '─'.repeat(maxNameLength + 22) + '┘'); console.log('└' + '─'.repeat(maxNameLength + 22) + '┘');
// Show completion percentage // Show completion percentage
const completedLevels = Math.max(0, currentLevelId - 1); const completedLevelsCount = completedLevels.length;
const completionPercentage = Math.round((completedLevels / allLevels.length) * 100); const completionPercentage = Math.round((completedLevelsCount / allLevels.length) * 100);
console.log(''); console.log('');
console.log(`Overall Progress: ${completedLevels}/${allLevels.length} levels completed (${completionPercentage}%)`); console.log(`Overall Progress: ${completedLevelsCount}/${allLevels.length} levels completed (${completionPercentage}%)`);
// Visual progress bar // Visual progress bar
const progressBarWidth = 40; const progressBarWidth = 40;