All checks were successful
Build / build (push) Successful in 1m21s
## Major Fixes ### 1. Fixed Invisible Meshes Issue - Root cause: Emissive materials require disableLighting=true without scene lighting - Added disableLighting=true to all loaded materials in loadAsset.ts - Scene intentionally uses no dynamic lights (space game with emissive textures) ### 2. Fixed CloudFlare Proxy + Vite Cache Issues - Updated vite.config.ts to pre-bundle BabylonJS procedural textures - Added force:false to prevent unnecessary cache invalidation - Fixed 504 Gateway Timeout errors on shader module dynamic imports - Separated babylon-procedural chunk for better caching ### 3. Responsive Design Improvements - Consolidated all CSS into public/styles.css with design tokens - Removed duplicate styles.css file - Created semantic header with navigation - Extracted 300+ lines of inline styles to CSS classes - Added mobile-first responsive breakpoints (320px, 480px, 768px, 1024px, 1440px) - Implemented fluid typography with clamp() ### 4. Level Progression System - Fixed level unlocking logic (tutorial always unlocked, others require auth) - Updated DEFAULT_LEVEL_ORDER to match actual level names - Made populateLevelSelector() async to properly await authentication - Added 3-column carousel layout for level selection - Visual states: locked, unlocked, current, completed ### 5. Discord Widget Management - Disabled Discord widget initialization (commented out) to prevent GraphQL errors - Added hide() call during gameplay - Can be re-enabled when Discord bot is properly configured ### 6. TypeScript Error Fixes - Removed unused hasSavedLevels import - Updated replay callbacks to use appHeader instead of individual link references - Fixed all TS compilation errors ## Files Modified - index.html - Semantic header, removed inline styles - public/styles.css - Consolidated styles with design tokens - src/gameConfig.ts - Enabled progression by default - src/levelSelector.ts - Fixed progression logic, async auth check - src/loginScreen.ts - Removed inline styles - src/main.ts - Discord handling, header visibility, error suppression - src/preloader.ts - Removed inline styles - src/progression.ts - Added isLevelUnlocked() method - src/utils/loadAsset.ts - Fixed emissive materials (disableLighting=true) - vite.config.ts - Pre-bundle procedural textures, prevent cache issues - styles.css - DELETED (consolidated into public/styles.css) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
264 lines
10 KiB
TypeScript
264 lines
10 KiB
TypeScript
import { getSavedLevels } from "./levelEditor";
|
|
import { LevelConfig } from "./levelConfig";
|
|
import { ProgressionManager } from "./progression";
|
|
import { GameConfig } from "./gameConfig";
|
|
import { AuthService } from "./authService";
|
|
import debugLog from './debug';
|
|
|
|
const SELECTED_LEVEL_KEY = 'space-game-selected-level';
|
|
|
|
// Default level order for the carousel
|
|
const DEFAULT_LEVEL_ORDER = [
|
|
'Rookie Training',
|
|
'Rescue Mission',
|
|
'Deep Space Patrol',
|
|
'Enemy Territory',
|
|
'The Gauntlet',
|
|
'Final Challenge'
|
|
];
|
|
|
|
/**
|
|
* Populate the level selection screen with saved levels
|
|
* Shows all 6 default levels in a 3x2 carousel with locked/unlocked states
|
|
*/
|
|
export async function populateLevelSelector(): Promise<boolean> {
|
|
const container = document.getElementById('levelCardsContainer');
|
|
if (!container) {
|
|
console.warn('Level cards container not found');
|
|
return false;
|
|
}
|
|
|
|
const savedLevels = getSavedLevels();
|
|
const gameConfig = GameConfig.getInstance();
|
|
const progressionEnabled = gameConfig.progressionEnabled;
|
|
const progression = ProgressionManager.getInstance();
|
|
|
|
if (savedLevels.size === 0) {
|
|
container.innerHTML = `
|
|
<div class="no-levels-message">
|
|
<h2>No Levels Found</h2>
|
|
<p>Something went wrong - default levels should be auto-generated!</p>
|
|
<a href="#/editor" class="btn-primary">Go to Level Editor</a>
|
|
</div>
|
|
`;
|
|
return false;
|
|
}
|
|
|
|
// Separate default and custom levels
|
|
const defaultLevels = new Map<string, LevelConfig>();
|
|
const customLevels = new Map<string, LevelConfig>();
|
|
|
|
for (const [name, config] of savedLevels.entries()) {
|
|
if (config.metadata?.type === 'default') {
|
|
defaultLevels.set(name, config);
|
|
} else {
|
|
customLevels.set(name, config);
|
|
}
|
|
}
|
|
|
|
let html = '';
|
|
|
|
// Show progression stats only if progression is enabled
|
|
if (progressionEnabled) {
|
|
const completedCount = progression.getCompletedCount();
|
|
const totalCount = progression.getTotalDefaultLevels();
|
|
const completionPercent = progression.getCompletionPercentage();
|
|
const nextLevel = progression.getNextLevel();
|
|
|
|
html += `
|
|
<div class="progress-bar-container" style="grid-column: 1 / -1;">
|
|
<h3 class="progress-bar-title">Progress</h3>
|
|
<div class="level-description">
|
|
${completedCount} of ${totalCount} default levels completed (${completionPercent.toFixed(0)}%)
|
|
</div>
|
|
<div class="progress-bar-track">
|
|
<div class="progress-fill" style="width: ${completionPercent}%;"></div>
|
|
</div>
|
|
${nextLevel ? `<div class="progress-percentage">Next: ${nextLevel}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Check if user is authenticated (ASYNC!)
|
|
const authService = AuthService.getInstance();
|
|
const isAuthenticated = await authService.isAuthenticated();
|
|
const isTutorial = (levelName: string) => levelName === DEFAULT_LEVEL_ORDER[0];
|
|
|
|
debugLog('[LevelSelector] Authenticated:', isAuthenticated);
|
|
debugLog('[LevelSelector] Progression enabled:', progressionEnabled);
|
|
debugLog('[LevelSelector] Tutorial level name:', DEFAULT_LEVEL_ORDER[0]);
|
|
debugLog('[LevelSelector] Default levels count:', defaultLevels.size);
|
|
debugLog('[LevelSelector] Default level names:', Array.from(defaultLevels.keys()));
|
|
|
|
// Show all 6 default levels in order (3x2 grid)
|
|
if (defaultLevels.size > 0) {
|
|
for (const levelName of DEFAULT_LEVEL_ORDER) {
|
|
const config = defaultLevels.get(levelName);
|
|
|
|
if (!config) {
|
|
// Level doesn't exist - show empty slot
|
|
html += `
|
|
<div class="level-card level-card-locked">
|
|
<div class="level-card-header">
|
|
<h2 class="level-card-title">${levelName}</h2>
|
|
<div class="level-card-status level-card-status-locked">🔒</div>
|
|
</div>
|
|
<div class="level-meta">Level not found</div>
|
|
<p class="level-card-description">This level has not been created yet.</p>
|
|
<button class="level-button" disabled>Locked</button>
|
|
</div>
|
|
`;
|
|
continue;
|
|
}
|
|
|
|
const description = config.metadata?.description || `${config.asteroids.length} asteroids • ${config.planets.length} planets`;
|
|
const estimatedTime = config.metadata?.estimatedTime || '';
|
|
const isCompleted = progressionEnabled && progression.isLevelComplete(levelName);
|
|
|
|
// Check if level is unlocked:
|
|
// - Tutorial is always unlocked
|
|
// - If authenticated: check progression unlock status
|
|
// - If not authenticated: only Tutorial is unlocked
|
|
let isUnlocked = false;
|
|
const isTut = isTutorial(levelName);
|
|
|
|
if (isTut) {
|
|
isUnlocked = true; // Tutorial always unlocked
|
|
debugLog(`[LevelSelector] ${levelName}: Tutorial - always unlocked`);
|
|
} else if (!isAuthenticated) {
|
|
isUnlocked = false; // Non-tutorial levels require authentication
|
|
debugLog(`[LevelSelector] ${levelName}: Not authenticated - locked`);
|
|
} else {
|
|
isUnlocked = !progressionEnabled || progression.isLevelUnlocked(levelName);
|
|
debugLog(`[LevelSelector] ${levelName}: Authenticated - unlocked:`, isUnlocked);
|
|
}
|
|
|
|
const isCurrentNext = progressionEnabled && progression.getNextLevel() === levelName;
|
|
|
|
// Determine card state
|
|
let cardClasses = 'level-card';
|
|
let statusIcon = '';
|
|
let buttonText = 'Play Level';
|
|
let buttonDisabled = '';
|
|
let lockReason = '';
|
|
|
|
if (isCompleted) {
|
|
cardClasses += ' level-card-completed';
|
|
statusIcon = '<div class="level-card-status level-card-status-complete">✓</div>';
|
|
buttonText = 'Replay';
|
|
} else if (isCurrentNext && isUnlocked) {
|
|
cardClasses += ' level-card-current';
|
|
statusIcon = '<div class="level-card-badge">START HERE</div>';
|
|
} else if (!isUnlocked) {
|
|
cardClasses += ' level-card-locked';
|
|
statusIcon = '<div class="level-card-status level-card-status-locked">🔒</div>';
|
|
|
|
// Determine why it's locked
|
|
if (!isAuthenticated && !isTutorial(levelName)) {
|
|
buttonText = 'Sign In Required';
|
|
lockReason = '<div class="level-lock-reason">Sign in to unlock</div>';
|
|
} else if (progressionEnabled) {
|
|
const levelIndex = DEFAULT_LEVEL_ORDER.indexOf(levelName);
|
|
if (levelIndex > 0) {
|
|
const previousLevel = DEFAULT_LEVEL_ORDER[levelIndex - 1];
|
|
lockReason = `<div class="level-lock-reason">Complete "${previousLevel}" to unlock</div>`;
|
|
}
|
|
buttonText = 'Locked';
|
|
} else {
|
|
buttonText = 'Locked';
|
|
}
|
|
buttonDisabled = ' disabled';
|
|
}
|
|
|
|
html += `
|
|
<div class="${cardClasses}">
|
|
<div class="level-card-header">
|
|
<h2 class="level-card-title">${levelName}</h2>
|
|
${statusIcon}
|
|
</div>
|
|
<div class="level-meta">
|
|
Difficulty: ${config.difficulty}${estimatedTime ? ` • ${estimatedTime}` : ''}
|
|
</div>
|
|
<p class="level-card-description">${description}</p>
|
|
${lockReason}
|
|
<button class="level-button" data-level="${levelName}"${buttonDisabled}>${buttonText}</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Show custom levels section if any exist
|
|
if (customLevels.size > 0) {
|
|
html += `
|
|
<div style="grid-column: 1 / -1; margin-top: var(--space-2xl);">
|
|
<h3 class="level-header">Custom Levels</h3>
|
|
</div>
|
|
`;
|
|
|
|
for (const [name, config] of customLevels.entries()) {
|
|
const description = config.metadata?.description || `${config.asteroids.length} asteroids • ${config.planets.length} planets`;
|
|
const author = config.metadata?.author ? ` by ${config.metadata.author}` : '';
|
|
|
|
html += `
|
|
<div class="level-card">
|
|
<div class="level-card-header">
|
|
<h2 class="level-card-title">${name}</h2>
|
|
</div>
|
|
<div class="level-meta">
|
|
Custom${author} • ${config.difficulty}
|
|
</div>
|
|
<p class="level-card-description">${description}</p>
|
|
<button class="level-button" data-level="${name}">Play Level</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
container.innerHTML = html;
|
|
|
|
// Attach event listeners to all level buttons
|
|
const buttons = container.querySelectorAll('.level-button:not([disabled])');
|
|
buttons.forEach(button => {
|
|
button.addEventListener('click', (e) => {
|
|
const target = e.target as HTMLButtonElement;
|
|
const levelName = target.getAttribute('data-level');
|
|
if (levelName) {
|
|
selectLevel(levelName);
|
|
}
|
|
});
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Select a level and dispatch event to start it
|
|
*/
|
|
export function selectLevel(levelName: string): void {
|
|
debugLog(`[LevelSelector] Level selected: ${levelName}`);
|
|
|
|
const savedLevels = getSavedLevels();
|
|
const config = savedLevels.get(levelName);
|
|
|
|
if (!config) {
|
|
console.error(`Level not found: ${levelName}`);
|
|
return;
|
|
}
|
|
|
|
// Save selected level
|
|
localStorage.setItem(SELECTED_LEVEL_KEY, levelName);
|
|
|
|
// Dispatch custom event that Main class will listen for
|
|
const event = new CustomEvent('levelSelected', {
|
|
detail: { levelName, config }
|
|
});
|
|
window.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Get the last selected level name
|
|
*/
|
|
export function getSelectedLevel(): string | null {
|
|
return localStorage.getItem(SELECTED_LEVEL_KEY);
|
|
}
|