๐ Ship Physics
-
+
Advanced tuning parameters for ship movement and handling. Adjust these to customize how the ship responds to controls.
@@ -422,20 +396,20 @@
โน๏ธ Quality Level Guide
-
-
Wireframe: Minimal rendering, shows mesh structure only. Best for debugging or very low-end devices.
-
Simple Material: Basic solid colors without textures. Good performance with basic visuals.
-
Full Texture: Standard textures with procedural generation. Recommended for most users.
-
PBR Texture: Physically-based rendering with enhanced materials. Best visual quality but higher GPU usage.
+
+
Wireframe: Minimal rendering, shows mesh structure only. Best for debugging or very low-end devices.
+
Simple Material: Basic solid colors without textures. Good performance with basic visuals.
+
Full Texture: Standard textures with procedural generation. Recommended for most users.
+
PBR Texture: Physically-based rendering with enhanced materials. Best visual quality but higher GPU usage.
๐พ Storage Info
-
+
Settings are automatically saved to your browser's local storage and will persist between sessions.
-
+
โ ๏ธ Note: Changes will take effect when you start a new level. Restart the current level to see changes.
@@ -447,13 +421,7 @@
๐ Reset to Defaults
-
+
diff --git a/public/styles.css b/public/styles.css
index 9d770ef..1400c18 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -1,39 +1,104 @@
+/* ============================================================================
+ CSS Custom Properties - Design System
+ ========================================================================= */
+:root {
+ /* Spacing Scale */
+ --space-xs: 4px;
+ --space-sm: 8px;
+ --space-md: 16px;
+ --space-lg: 24px;
+ --space-xl: 32px;
+ --space-2xl: 48px;
+ --space-3xl: 64px;
+
+ /* Color Palette */
+ --color-primary: #667eea;
+ --color-primary-dark: #764ba2;
+ --color-secondary: #00d4ff;
+ --color-success: #4CAF50;
+ --color-danger: #FF6B6B;
+ --color-warning: #FF9800;
+
+ /* Text Colors */
+ --color-text-primary: #ffffff;
+ --color-text-secondary: #e8e8e8;
+ --color-text-muted: #aaaaaa;
+ --color-text-disabled: #666666;
+
+ /* Background Colors */
+ --color-bg-dark: #000000;
+ --color-bg-card: rgba(20, 20, 40, 0.9);
+ --color-bg-overlay: rgba(255, 255, 255, 0.1);
+ --color-bg-overlay-hover: rgba(255, 255, 255, 0.15);
+
+ /* Border Colors */
+ --color-border-default: rgba(255, 255, 255, 0.2);
+ --color-border-hover: rgba(255, 255, 255, 0.4);
+ --color-border-primary: rgba(0, 150, 255, 0.5);
+
+ /* Gradients */
+ --gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ --gradient-secondary: linear-gradient(135deg, #0066cc, #0099ff);
+ --gradient-success: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
+ --gradient-danger: linear-gradient(135deg, #FF6B6B 0%, #C92A2A 100%);
+ --gradient-dark: linear-gradient(135deg, #434343 0%, #000000 100%);
+ --gradient-bg: linear-gradient(135deg, #0a0618, #1a1033, #0f0c29);
+
+ /* Z-index Layers */
+ --z-base: 0;
+ --z-header: 1000;
+ --z-nav: 2000;
+ --z-overlay: 5000;
+ --z-modal: 10000;
+
+ /* Typography Scale */
+ --font-size-xs: clamp(0.75rem, 2vw, 0.85rem);
+ --font-size-sm: clamp(0.85rem, 2vw, 0.95rem);
+ --font-size-base: clamp(0.95rem, 2.5vw, 1.1rem);
+ --font-size-lg: clamp(1.1rem, 3vw, 1.3rem);
+ --font-size-xl: clamp(1.3rem, 3.5vw, 1.6rem);
+ --font-size-2xl: clamp(1.5rem, 4vw, 2rem);
+ --font-size-3xl: clamp(1.8rem, 5vw, 2.5rem);
+ --font-size-4xl: clamp(2rem, 6vw, 3rem);
+
+ /* Transitions */
+ --transition-fast: 0.2s ease;
+ --transition-base: 0.3s ease;
+ --transition-slow: 0.5s ease;
+
+ /* Border Radius */
+ --radius-sm: 5px;
+ --radius-md: 8px;
+ --radius-lg: 10px;
+ --radius-xl: 12px;
+ --radius-2xl: 15px;
+
+ /* Shadows */
+ --shadow-sm: 0 4px 6px rgba(0, 0, 0, 0.3);
+ --shadow-md: 0 6px 12px rgba(0, 0, 0, 0.3);
+ --shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.3);
+ --shadow-xl: 0 10px 20px rgba(0, 0, 0, 0.3);
+}
+
+/* ============================================================================
+ Global Styles
+ ========================================================================= */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
body {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
- background-color: #000;
- color: #fff;
- aspect-ratio: auto;
+ background-color: var(--color-bg-dark);
+ color: var(--color-text-primary);
font-family: Roboto, sans-serif;
- font-size: large;
-}
-
-#startButton {
- background-color: #000;
- color: #fff;
- border: 2px solid #fff;
- border-radius: 5px;
- cursor: pointer;
- font-size: xxx-large;
- display: none;
- z-index: 1000;
-}
-#startButton.ready {
- display: block;
- background-color: red;
-}
-#loadingDiv {
- z-index: 1000;
- color: #fff;
-}
-#mainDiv {
- position: absolute;
- display: block;
- top: 48px;
- z-index: 1000;
- overflow: scroll;
+ font-size: 16px;
+ overflow-x: hidden;
}
#gameCanvas {
@@ -44,180 +109,57 @@ body {
background: transparent;
}
-#levelSelect {
- display: none;
- text-align: center;
- color: #fff;
-}
-
-#levelSelect.ready {
- display: block;
-}
-
-#levelSelect h1 {
- font-size: 2.5em;
- margin-bottom: 30px;
- text-shadow: 0 0 10px rgba(255,255,255,0.5);
-}
-
-.controls-info {
- background: rgba(255, 255, 255, 0.1);
- border: 2px solid rgba(255, 255, 255, 0.2);
- border-radius: 15px;
- padding: 30px;
- margin: 0 auto 40px;
- max-width: 900px;
+/* ============================================================================
+ Header Layout
+ ========================================================================= */
+.app-header {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: var(--z-header);
+ background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
+ border-bottom: 1px solid var(--color-border-default);
}
-.controls-info h2 {
- margin-top: 0;
- margin-bottom: 25px;
- font-size: 2em;
- color: #4CAF50;
- text-shadow: 0 0 10px rgba(76, 175, 80, 0.5);
-}
-
-.controls-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
- gap: 25px;
- margin-bottom: 20px;
-}
-
-.control-section {
- background: rgba(0, 0, 0, 0.3);
- border: 1px solid rgba(255, 255, 255, 0.15);
- border-radius: 10px;
- padding: 20px;
-}
-
-.control-section h3 {
- margin-top: 0;
- margin-bottom: 15px;
- color: #64B5F6;
- font-size: 1.3em;
-}
-
-.control-section ul {
- list-style: none;
- padding: 0;
- margin: 0;
-}
-
-.control-section li {
- margin-bottom: 12px;
- padding-left: 20px;
- position: relative;
- line-height: 1.6;
- font-size: 0.95em;
-}
-
-
-.control-section strong {
- color: #FFD54F;
-}
-
-.controls-note {
- background: rgba(255, 152, 0, 0.15);
- border: 1px solid rgba(255, 152, 0, 0.3);
- border-radius: 8px;
- padding: 15px;
- margin-top: 20px;
- font-size: 0.9em;
- line-height: 1.6;
- color: #FFE082;
-}
-
-.card-container {
+.header-content {
display: flex;
- flex-wrap: wrap;
- gap: 20px;
- justify-content: center;
- max-width: 1200px;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-md) var(--space-lg);
+ max-width: 1400px;
margin: 0 auto;
}
-.level-card {
- background: rgba(255, 255, 255, 0.1);
- border: 2px solid rgba(255, 255, 255, 0.2);
- border-radius: 10px;
- padding: 20px;
- width: 250px;
- transition: all 0.3s;
- backdrop-filter: blur(10px);
+.header-left {
+ flex-shrink: 0;
}
-.level-card:hover {
- background: rgba(255, 255, 255, 0.15);
- border-color: rgba(255, 255, 255, 0.4);
- transform: translateY(-5px);
- box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
-}
-
-.level-card h2 {
- margin-top: 0;
- color: #4CAF50;
-}
-
-.level-card p {
- font-size: 0.9em;
- color: #ccc;
- margin: 15px 0;
-}
-
-.level-button {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- border: none;
- padding: 12px 24px;
- border-radius: 5px;
- cursor: pointer;
- font-size: 1em;
+.app-title {
+ margin: 0;
+ font-size: var(--font-size-lg);
font-weight: bold;
- transition: all 0.2s;
+ color: var(--color-text-primary);
}
-.level-button:hover {
- transform: scale(1.05);
- box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
+.header-nav {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
}
-.test-level-button {
- background: linear-gradient(135deg, #FF6B6B 0%, #C92A2A 100%);
- color: white;
- border: 2px solid rgba(255, 107, 107, 0.5);
- padding: 12px 30px;
- border-radius: 8px;
- cursor: pointer;
- font-size: 1.1em;
- font-weight: bold;
- transition: all 0.3s;
- margin-bottom: 15px;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
-}
-
-.test-level-button:hover {
- transform: scale(1.05);
- box-shadow: 0 6px 20px rgba(255, 107, 107, 0.6);
- border-color: rgba(255, 107, 107, 0.8);
-}
-
-.editor-link,
-.settings-link {
- position: absolute;
- top: 20px;
- color: white;
- padding: 10px 20px;
- border-radius: 5px;
+.nav-link {
+ padding: var(--space-sm) var(--space-lg);
+ border-radius: var(--radius-sm);
text-decoration: none;
font-weight: bold;
- transition: all 0.3s;
- z-index: 2000;
+ color: var(--color-text-primary);
+ transition: all var(--transition-base);
+ white-space: nowrap;
}
.editor-link {
- right: 180px;
background: rgba(76, 175, 80, 0.8);
}
@@ -227,7 +169,6 @@ body {
}
.settings-link {
- right: 20px;
background: rgba(33, 150, 243, 0.8);
}
@@ -236,35 +177,689 @@ body {
transform: scale(1.05);
}
-/* Editor View Styles */
+/* User Profile in Header */
+.user-profile {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+ padding: var(--space-sm) var(--space-md);
+ background: var(--color-bg-overlay);
+ border: 1px solid var(--color-border-default);
+ border-radius: var(--radius-lg);
+}
+
+.user-profile-name {
+ color: var(--color-text-secondary);
+ font-weight: 500;
+}
+
+.user-profile-button {
+ padding: var(--space-sm) var(--space-md);
+ background: var(--gradient-primary);
+ color: var(--color-text-primary);
+ border: none;
+ border-radius: var(--radius-sm);
+ font-weight: bold;
+ cursor: pointer;
+ transition: all var(--transition-base);
+}
+
+.user-profile-button:hover {
+ transform: scale(1.05);
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.6);
+}
+
+/* ============================================================================
+ Main Content Area
+ ========================================================================= */
+#mainDiv {
+ position: absolute;
+ top: 80px; /* Below header */
+ left: 0;
+ right: 0;
+ z-index: var(--z-base);
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding: 0 var(--space-md);
+}
+
+#loadingDiv {
+ z-index: var(--z-header);
+ color: var(--color-text-primary);
+ text-align: center;
+ padding: var(--space-xl);
+}
+
+#startButton {
+ background-color: var(--color-bg-dark);
+ color: var(--color-text-primary);
+ border: 2px solid var(--color-text-primary);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ font-size: var(--font-size-3xl);
+ padding: var(--space-lg) var(--space-3xl);
+ display: none;
+ z-index: var(--z-header);
+ transition: all var(--transition-base);
+}
+
+#startButton.ready {
+ display: block;
+ background-color: var(--color-danger);
+}
+
+#startButton:hover {
+ transform: scale(1.05);
+ box-shadow: var(--shadow-lg);
+}
+
+/* ============================================================================
+ Level Selection
+ ========================================================================= */
+#levelSelect {
+ display: none;
+ text-align: center;
+ color: var(--color-text-primary);
+ opacity: 0;
+ transition: opacity var(--transition-slow);
+ padding: var(--space-lg) 0;
+}
+
+#levelSelect.ready {
+ display: block;
+ opacity: 1;
+}
+
+/* Hero Section */
+.hero {
+ margin-bottom: var(--space-2xl);
+}
+
+.hero-title {
+ font-size: var(--font-size-4xl);
+ margin: 0 0 var(--space-md) 0;
+ background: var(--gradient-primary);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ font-weight: bold;
+}
+
+.hero-subtitle {
+ font-size: var(--font-size-xl);
+ color: var(--color-text-muted);
+ margin: 0;
+}
+
+/* Level Section */
+.level-section {
+ margin-bottom: var(--space-2xl);
+}
+
+.level-header {
+ font-size: var(--font-size-2xl);
+ color: var(--color-text-primary);
+ margin: 0 0 var(--space-md) 0;
+}
+
+.level-description {
+ font-size: var(--font-size-base);
+ color: var(--color-text-muted);
+ margin: 0 0 var(--space-xl) 0;
+}
+
+/* Progress Bar */
+.progress-bar-container {
+ background: var(--color-bg-overlay);
+ border: 1px solid var(--color-border-default);
+ border-radius: var(--radius-xl);
+ padding: var(--space-lg);
+ margin: 0 auto var(--space-xl);
+ max-width: 600px;
+}
+
+.progress-bar-title {
+ font-size: var(--font-size-base);
+ color: var(--color-text-secondary);
+ margin: 0 0 var(--space-md) 0;
+}
+
+.progress-bar-track {
+ width: 100%;
+ height: 12px;
+ background: rgba(0, 0, 0, 0.5);
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+ margin-bottom: var(--space-sm);
+}
+
+.progress-fill {
+ height: 100%;
+ background: var(--gradient-primary);
+ transition: width var(--transition-base);
+ box-shadow: 0 0 10px rgba(102, 126, 234, 0.5);
+}
+
+.progress-percentage {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-muted);
+ margin: 0;
+}
+
+/* Level Cards */
+.card-container {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: var(--space-lg);
+ max-width: 1200px;
+ margin: 0 auto var(--space-2xl);
+ padding: 0 var(--space-lg);
+}
+
+.no-levels-message {
+ grid-column: 1 / -1;
+ text-align: center;
+ padding: var(--space-2xl) var(--space-lg);
+ color: var(--color-text-muted);
+}
+
+.no-levels-message h2 {
+ margin-bottom: var(--space-lg);
+}
+
+.no-levels-message p {
+ margin-bottom: var(--space-xl);
+}
+
+.level-card {
+ background: var(--color-bg-card);
+ border: 2px solid var(--color-border-primary);
+ border-radius: var(--radius-xl);
+ padding: var(--space-xl) var(--space-lg);
+ transition: all var(--transition-base);
+ box-shadow: var(--shadow-sm);
+}
+
+.level-card:hover {
+ transform: translateY(-5px);
+ border-color: var(--color-secondary);
+ box-shadow: 0 8px 16px rgba(0, 150, 255, 0.4);
+}
+
+.level-card-locked {
+ background: rgba(20, 20, 40, 0.5);
+ border: 2px solid rgba(255, 255, 255, 0.1);
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.level-card-locked:hover {
+ transform: none;
+ border-color: rgba(255, 255, 255, 0.1);
+ box-shadow: var(--shadow-sm);
+}
+
+.level-card-completed {
+ border-color: #4ade80;
+ box-shadow: 0 4px 12px rgba(74, 222, 128, 0.3);
+}
+
+.level-card-current {
+ border: 2px solid #667eea;
+ box-shadow: 0 0 20px rgba(102, 126, 234, 0.3);
+ animation: pulse 2s ease-in-out infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ box-shadow: 0 0 20px rgba(102, 126, 234, 0.3);
+ }
+ 50% {
+ box-shadow: 0 0 30px rgba(102, 126, 234, 0.6);
+ }
+}
+
+.level-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: start;
+ margin-bottom: var(--space-md);
+}
+
+.level-card-title {
+ color: var(--color-secondary);
+ font-size: var(--font-size-2xl);
+ margin: 0;
+ text-transform: uppercase;
+}
+
+.level-card-locked .level-card-title {
+ color: var(--color-text-disabled);
+}
+
+.level-card-badge {
+ font-size: var(--font-size-xs);
+ background: #667eea;
+ padding: 4px 8px;
+ border-radius: 4px;
+ color: white;
+ font-weight: bold;
+}
+
+.level-card-status {
+ font-size: 1.5em;
+}
+
+.level-card-status-complete {
+ color: #4ade80;
+}
+
+.level-card-status-locked {
+ color: var(--color-text-disabled);
+}
+
+.level-lock-reason {
+ font-size: var(--font-size-sm);
+ color: #FF9800;
+ margin: var(--space-sm) 0;
+ padding: var(--space-sm);
+ background: rgba(255, 152, 0, 0.1);
+ border-radius: var(--radius-sm);
+ border-left: 3px solid #FF9800;
+}
+
+.level-meta {
+ color: var(--color-text-muted);
+ font-size: var(--font-size-sm);
+ margin: 0 0 var(--space-md) 0;
+}
+
+.level-card-description {
+ color: var(--color-text-muted);
+ font-size: var(--font-size-base);
+ line-height: 1.6;
+ margin: 0 0 var(--space-lg) 0;
+ min-height: 50px;
+}
+
+.level-button {
+ background: var(--gradient-secondary);
+ color: var(--color-text-primary);
+ border: none;
+ padding: var(--space-md) var(--space-xl);
+ font-size: var(--font-size-base);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: all var(--transition-base);
+ font-weight: bold;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ width: 100%;
+}
+
+.level-button:hover {
+ background: linear-gradient(135deg, #0088ff, #00bbff);
+ box-shadow: 0 4px 12px rgba(0, 150, 255, 0.6);
+ transform: scale(1.05);
+}
+
+.level-button:active {
+ transform: scale(0.98);
+}
+
+.level-button:focus-visible {
+ outline: 2px solid var(--color-secondary);
+ outline-offset: 2px;
+}
+
+/* Test Level Button */
+.test-level-button {
+ background: var(--gradient-danger);
+ color: var(--color-text-primary);
+ border: 2px solid rgba(255, 107, 107, 0.5);
+ padding: var(--space-md) var(--space-xl);
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ font-size: var(--font-size-base);
+ font-weight: bold;
+ transition: all var(--transition-base);
+ margin-bottom: var(--space-md);
+ box-shadow: var(--shadow-sm);
+}
+
+.test-level-button:hover {
+ transform: scale(1.05);
+ box-shadow: 0 6px 20px rgba(255, 107, 107, 0.6);
+ border-color: rgba(255, 107, 107, 0.8);
+}
+
+.test-level-button:focus-visible {
+ outline: 2px solid var(--color-danger);
+ outline-offset: 2px;
+}
+
+/* ============================================================================
+ Controls Info Section
+ ========================================================================= */
+.controls-info {
+ background: var(--color-bg-overlay);
+ border: 2px solid var(--color-border-default);
+ border-radius: var(--radius-2xl);
+ padding: var(--space-xl);
+ margin: 0 auto var(--space-2xl);
+ max-width: 900px;
+ backdrop-filter: blur(10px);
+}
+
+.controls-info summary {
+ cursor: pointer;
+ font-size: var(--font-size-2xl);
+ color: var(--color-success);
+ text-shadow: 0 0 10px rgba(76, 175, 80, 0.5);
+ margin-bottom: var(--space-lg);
+ list-style: none;
+ padding: var(--space-md);
+ border-radius: var(--radius-md);
+ transition: all var(--transition-base);
+}
+
+.controls-info summary:hover {
+ background: var(--color-bg-overlay);
+}
+
+.controls-info summary:focus-visible {
+ outline: 2px solid var(--color-success);
+ outline-offset: 2px;
+}
+
+.controls-info h2 {
+ margin: 0 0 var(--space-lg) 0;
+ font-size: var(--font-size-2xl);
+ color: var(--color-success);
+ text-shadow: 0 0 10px rgba(76, 175, 80, 0.5);
+}
+
+.controls-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: var(--space-lg);
+ margin-bottom: var(--space-lg);
+}
+
+.control-section {
+ background: rgba(0, 0, 0, 0.3);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: var(--radius-lg);
+ padding: var(--space-lg);
+}
+
+.control-section h3 {
+ margin: 0 0 var(--space-md) 0;
+ color: #64B5F6;
+ font-size: var(--font-size-lg);
+}
+
+.control-section ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.control-section li {
+ margin-bottom: var(--space-md);
+ padding-left: var(--space-lg);
+ position: relative;
+ line-height: 1.6;
+ font-size: var(--font-size-sm);
+}
+
+.control-section li::before {
+ content: "โธ";
+ position: absolute;
+ left: 0;
+ color: var(--color-secondary);
+}
+
+.control-section strong {
+ color: #FFD54F;
+}
+
+.controls-note {
+ background: rgba(255, 152, 0, 0.15);
+ border: 1px solid rgba(255, 152, 0, 0.3);
+ border-radius: var(--radius-md);
+ padding: var(--space-md);
+ margin-top: var(--space-lg);
+ font-size: var(--font-size-sm);
+ line-height: 1.6;
+ color: #FFE082;
+}
+
+/* ============================================================================
+ Login Screen
+ ========================================================================= */
+.login-screen {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(0, 0, 0, 0.95);
+ z-index: var(--z-modal);
+}
+
+.login-container {
+ text-align: center;
+ padding: var(--space-2xl);
+ background: var(--color-bg-overlay);
+ border: 2px solid var(--color-border-default);
+ border-radius: var(--radius-2xl);
+ max-width: 500px;
+ width: 90%;
+}
+
+.login-title {
+ font-size: var(--font-size-3xl);
+ margin: 0 0 var(--space-lg) 0;
+ background: var(--gradient-primary);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.login-subtitle {
+ font-size: var(--font-size-base);
+ color: var(--color-text-muted);
+ margin: 0 0 var(--space-xl) 0;
+}
+
+.login-button {
+ padding: var(--space-md) var(--space-2xl);
+ background: var(--gradient-primary);
+ color: var(--color-text-primary);
+ border: none;
+ border-radius: var(--radius-lg);
+ font-size: var(--font-size-base);
+ font-weight: bold;
+ cursor: pointer;
+ transition: all var(--transition-base);
+ box-shadow: 0 6px 25px rgba(102, 126, 234, 0.5);
+}
+
+.login-button:hover {
+ transform: translateY(-3px) scale(1.05);
+ box-shadow: 0 8px 30px rgba(102, 126, 234, 0.7);
+}
+
+.login-button:focus-visible {
+ outline: 2px solid var(--color-primary);
+ outline-offset: 2px;
+}
+
+.login-skip {
+ display: block;
+ margin-top: var(--space-lg);
+ color: var(--color-text-muted);
+ text-decoration: underline;
+ cursor: pointer;
+ font-size: var(--font-size-sm);
+}
+
+.login-skip:hover {
+ color: var(--color-text-primary);
+}
+
+/* ============================================================================
+ Preloader
+ ========================================================================= */
+.preloader {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: rgba(0, 0, 0, 0.95);
+ z-index: var(--z-modal);
+ padding: var(--space-lg);
+}
+
+.preloader-content {
+ text-align: center;
+ max-width: 600px;
+ width: 100%;
+}
+
+.preloader-title {
+ font-size: var(--font-size-4xl);
+ margin-bottom: var(--space-lg);
+ background: var(--gradient-primary);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.preloader-status {
+ font-size: var(--font-size-lg);
+ color: var(--color-text-muted);
+ margin: var(--space-xl) 0 var(--space-lg) 0;
+ min-height: 30px;
+}
+
+.preloader-progress-container {
+ width: 100%;
+ height: 12px;
+ background: var(--color-bg-overlay);
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+ margin-bottom: var(--space-2xl);
+}
+
+.preloader-progress {
+ width: 0%;
+ height: 100%;
+ background: var(--gradient-primary);
+ transition: width var(--transition-base);
+ box-shadow: 0 0 10px rgba(102, 126, 234, 0.5);
+}
+
+.preloader-button {
+ display: none;
+ padding: var(--space-lg) var(--space-3xl);
+ background: var(--gradient-primary);
+ color: var(--color-text-primary);
+ border: none;
+ border-radius: var(--radius-lg);
+ font-size: var(--font-size-xl);
+ font-weight: bold;
+ cursor: pointer;
+ transition: all var(--transition-base);
+ box-shadow: 0 6px 25px rgba(102, 126, 234, 0.5);
+}
+
+.preloader-button:hover {
+ transform: translateY(-3px) scale(1.05);
+ box-shadow: 0 8px 30px rgba(102, 126, 234, 0.7);
+}
+
+.preloader-button:focus-visible {
+ outline: 2px solid var(--color-primary);
+ outline-offset: 2px;
+}
+
+.preloader-info {
+ margin-top: var(--space-xl);
+ font-size: var(--font-size-sm);
+ color: var(--color-text-disabled);
+}
+
+/* ============================================================================
+ Test Buttons & Links
+ ========================================================================= */
+.test-buttons-container {
+ text-align: center;
+ margin-top: var(--space-lg);
+}
+
+.test-buttons-container .test-level-button {
+ margin: 0 var(--space-sm) var(--space-md);
+}
+
+.level-create-link {
+ color: var(--color-success);
+ text-decoration: none;
+ font-size: var(--font-size-base);
+ font-weight: 600;
+}
+
+.level-create-link:hover {
+ text-decoration: underline;
+ color: #5ED35A;
+}
+
+.level-create-link:focus-visible {
+ outline: 2px solid var(--color-success);
+ outline-offset: 2px;
+}
+
+/* ============================================================================
+ Editor & Settings Views
+ ========================================================================= */
[data-view="editor"],
[data-view="settings"] {
- background: linear-gradient(135deg, #0a0618, #1a1033, #0f0c29);
+ background: var(--gradient-bg);
min-height: 100vh;
- padding: 15px;
+ padding: var(--space-md);
overflow-y: auto;
}
.editor-container {
max-width: 1200px;
margin: 0 auto;
- color: #fff;
+ color: var(--color-text-primary);
+ padding-top: 80px; /* Account for header */
}
.editor-container h1 {
text-align: center;
- font-size: clamp(1.8em, 5vw, 2.5em);
- margin-bottom: 10px;
+ font-size: var(--font-size-3xl);
+ margin-bottom: var(--space-sm);
text-shadow: 0 0 15px rgba(255,255,255,0.8);
- color: #ffffff;
+ color: var(--color-text-primary);
font-weight: bold;
}
.subtitle {
text-align: center;
- color: #d0d0d0;
- margin-bottom: 30px;
- font-size: clamp(0.9em, 2.5vw, 1.1em);
+ color: var(--color-text-secondary);
+ margin-bottom: var(--space-xl);
+ font-size: var(--font-size-base);
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
}
@@ -272,39 +867,39 @@ body {
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
- gap: 15px;
- margin-bottom: 20px;
+ gap: var(--space-md);
+ margin-bottom: var(--space-lg);
}
.section {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
- border-radius: 10px;
- padding: 20px;
+ border-radius: var(--radius-lg);
+ padding: var(--space-lg);
backdrop-filter: blur(10px);
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
+ box-shadow: var(--shadow-sm);
}
.section h2 {
margin-top: 0;
- font-size: clamp(1.2em, 3vw, 1.5em);
+ font-size: var(--font-size-xl);
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
- padding-bottom: 10px;
- margin-bottom: 20px;
- color: #ffffff;
+ padding-bottom: var(--space-sm);
+ margin-bottom: var(--space-lg);
+ color: var(--color-text-primary);
font-weight: bold;
text-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
.form-group {
- margin-bottom: 18px;
+ margin-bottom: var(--space-lg);
}
.form-group label {
display: block;
- margin-bottom: 8px;
- font-size: 0.95em;
- color: #e8e8e8;
+ margin-bottom: var(--space-sm);
+ font-size: var(--font-size-sm);
+ color: var(--color-text-secondary);
font-weight: 500;
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
}
@@ -313,14 +908,13 @@ body {
.form-group input[type="number"],
.form-group select {
width: 100%;
- padding: 12px;
+ padding: var(--space-md);
background: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.3);
- border-radius: 5px;
- color: #ffffff;
+ border-radius: var(--radius-sm);
+ color: var(--color-text-primary);
font-size: 1em;
- box-sizing: border-box;
- transition: all 0.2s;
+ transition: all var(--transition-fast);
}
.form-group input::placeholder {
@@ -330,20 +924,26 @@ body {
.form-group input:focus,
.form-group select:focus {
outline: none;
- border-color: #4CAF50;
+ border-color: var(--color-success);
box-shadow: 0 0 8px rgba(76, 175, 80, 0.6);
background: rgba(0, 0, 0, 0.6);
}
+.form-group input:focus-visible,
+.form-group select:focus-visible {
+ outline: 2px solid var(--color-success);
+ outline-offset: 1px;
+}
+
.form-group select option {
background: #1a1a1a;
- color: #ffffff;
+ color: var(--color-text-primary);
}
.vector-input {
display: grid;
grid-template-columns: repeat(3, 1fr);
- gap: 10px;
+ gap: var(--space-sm);
}
.vector-input input {
@@ -352,38 +952,38 @@ body {
.vector-label {
text-align: center;
- font-size: 0.85em;
- color: #d0d0d0;
- margin-bottom: 5px;
+ font-size: var(--font-size-xs);
+ color: var(--color-text-secondary);
+ margin-bottom: var(--space-xs);
font-weight: 600;
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
}
.button-group {
display: flex;
- gap: 12px;
+ gap: var(--space-md);
justify-content: center;
- margin-top: 30px;
+ margin-top: var(--space-xl);
flex-wrap: wrap;
}
.btn-primary,
.btn-success,
.btn-secondary {
- padding: 14px 28px;
- font-size: clamp(0.95em, 2.5vw, 1.1em);
+ padding: var(--space-md) var(--space-lg);
+ font-size: var(--font-size-base);
border: none;
- border-radius: 6px;
+ border-radius: var(--radius-sm);
cursor: pointer;
- transition: all 0.3s;
+ transition: all var(--transition-base);
font-weight: bold;
- color: white;
+ color: var(--color-text-primary);
min-width: 140px;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
.btn-primary {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ background: var(--gradient-primary);
border: 2px solid rgba(102, 126, 234, 0.5);
}
@@ -393,8 +993,13 @@ body {
border-color: rgba(102, 126, 234, 0.8);
}
+.btn-primary:focus-visible {
+ outline: 2px solid var(--color-primary);
+ outline-offset: 2px;
+}
+
.btn-success {
- background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
+ background: var(--gradient-success);
border: 2px solid rgba(56, 239, 125, 0.5);
}
@@ -404,8 +1009,13 @@ body {
border-color: rgba(56, 239, 125, 0.8);
}
+.btn-success:focus-visible {
+ outline: 2px solid var(--color-success);
+ outline-offset: 2px;
+}
+
.btn-secondary {
- background: linear-gradient(135deg, #434343 0%, #000000 100%);
+ background: var(--gradient-dark);
border: 2px solid rgba(255, 255, 255, 0.2);
}
@@ -415,22 +1025,27 @@ body {
border-color: rgba(255, 255, 255, 0.4);
}
+.btn-secondary:focus-visible {
+ outline: 2px solid var(--color-text-primary);
+ outline-offset: 2px;
+}
+
.preset-buttons {
display: flex;
- gap: 10px;
+ gap: var(--space-sm);
flex-wrap: wrap;
- margin-bottom: 20px;
+ margin-bottom: var(--space-lg);
}
.preset-btn {
- padding: 10px 18px;
- font-size: 0.95em;
+ padding: var(--space-sm) var(--space-lg);
+ font-size: var(--font-size-sm);
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.25);
- border-radius: 5px;
- color: #ffffff;
+ border-radius: var(--radius-sm);
+ color: var(--color-text-primary);
cursor: pointer;
- transition: all 0.2s;
+ transition: all var(--transition-fast);
font-weight: 500;
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
}
@@ -441,56 +1056,61 @@ body {
transform: scale(1.02);
}
+.preset-btn:focus-visible {
+ outline: 2px solid var(--color-text-primary);
+ outline-offset: 2px;
+}
+
.preset-btn.active {
- background: #4CAF50;
- border-color: #4CAF50;
+ background: var(--color-success);
+ border-color: var(--color-success);
box-shadow: 0 3px 8px rgba(76, 175, 80, 0.4);
}
.help-text {
- font-size: 0.85em;
- color: #c0c0c0;
- margin-top: 6px;
+ font-size: var(--font-size-xs);
+ color: var(--color-text-muted);
+ margin-top: var(--space-xs);
text-shadow: 0 1px 2px rgba(0,0,0,0.8);
}
.output-section {
background: rgba(0, 0, 0, 0.5);
- border: 1px solid rgba(255, 255, 255, 0.2);
- border-radius: 10px;
- padding: 20px;
- margin-top: 30px;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
+ border: 1px solid var(--color-border-default);
+ border-radius: var(--radius-lg);
+ padding: var(--space-lg);
+ margin-top: var(--space-xl);
+ box-shadow: var(--shadow-sm);
}
.output-section h2 {
margin-top: 0;
- color: #ffffff;
+ color: var(--color-text-primary);
font-weight: bold;
text-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
#jsonOutput {
background: #0a0a0a;
- border: 1px solid rgba(255, 255, 255, 0.2);
- border-radius: 5px;
- padding: 15px;
+ border: 1px solid var(--color-border-default);
+ border-radius: var(--radius-sm);
+ padding: var(--space-md);
max-height: 400px;
overflow-y: auto;
font-family: 'Courier New', monospace;
- font-size: clamp(0.75em, 2vw, 0.85em);
+ font-size: var(--font-size-xs);
white-space: pre-wrap;
word-wrap: break-word;
- color: #e0e0e0;
+ color: var(--color-text-secondary);
line-height: 1.5;
}
.back-link {
display: inline-block;
- margin-bottom: 20px;
- color: #4CAF50;
+ margin-bottom: var(--space-lg);
+ color: var(--color-success);
text-decoration: none;
- font-size: clamp(1em, 2.5vw, 1.1em);
+ font-size: var(--font-size-base);
font-weight: 600;
text-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
@@ -500,69 +1120,96 @@ body {
color: #5ED35A;
}
-/* Mobile-specific adjustments */
-@media (max-width: 768px) {
- .controls-info {
- padding: 20px;
- margin-bottom: 30px;
- }
+.back-link:focus-visible {
+ outline: 2px solid var(--color-success);
+ outline-offset: 2px;
+}
- .controls-info h2 {
- font-size: 1.5em;
- }
+/* Editor JSON Output */
+.editor-json-output {
+ margin-top: var(--space-2xl);
+}
- .controls-grid {
- grid-template-columns: 1fr;
- gap: 15px;
- }
+.editor-json-note {
+ color: var(--color-text-muted);
+ font-size: var(--font-size-sm);
+ margin-bottom: var(--space-md);
+}
- .control-section h3 {
- font-size: 1.1em;
- }
+.json-editor-textarea {
+ width: 100%;
+ min-height: 400px;
+ background: #0a0a0a;
+ border: 1px solid var(--color-border-default);
+ border-radius: var(--radius-sm);
+ padding: var(--space-md);
+ font-family: 'Courier New', monospace;
+ font-size: var(--font-size-xs);
+ color: var(--color-text-secondary);
+ line-height: 1.5;
+ resize: vertical;
+}
- .control-section li {
- font-size: 0.9em;
- }
+.editor-json-buttons {
+ display: flex;
+ gap: var(--space-sm);
+ margin-top: var(--space-md);
+ justify-content: flex-end;
+}
- .controls-note {
- font-size: 0.85em;
- }
+.json-validation-message {
+ margin-top: var(--space-sm);
+ font-size: var(--font-size-sm);
+}
- [data-view="editor"] {
- padding: 10px;
- }
+/* Settings View Specific */
+.settings-description {
+ color: var(--color-text-muted);
+ font-size: var(--font-size-sm);
+ margin-bottom: var(--space-lg);
+ line-height: 1.6;
+}
- .editor-container h1 {
- margin-bottom: 5px;
- }
+.checkbox-label {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ user-select: none;
+}
- .subtitle {
- margin-bottom: 20px;
- }
+.settings-checkbox {
+ width: 20px;
+ height: 20px;
+ margin-right: var(--space-sm);
+ cursor: pointer;
+ accent-color: var(--color-success);
+}
- .section {
- padding: 15px;
- }
+.settings-info-content {
+ color: var(--color-text-muted);
+ font-size: var(--font-size-sm);
+ line-height: 1.8;
+}
- .editor-grid {
- gap: 12px;
- }
+.settings-info-content p {
+ margin: 0 0 var(--space-md) 0;
+}
- .button-group {
- gap: 10px;
- }
+.settings-label {
+ color: var(--color-success);
+}
- .btn-primary,
- .btn-success,
- .btn-secondary {
- padding: 12px 20px;
- min-width: 120px;
- }
+.settings-warning {
+ color: #FF9800;
+ margin-top: var(--space-sm);
+}
- .preset-btn {
- padding: 8px 14px;
- font-size: 0.9em;
- }
+.settings-message {
+ text-align: center;
+ margin-top: var(--space-lg);
+ font-size: var(--font-size-base);
+ opacity: 0;
+ transition: opacity var(--transition-base);
}
/* Saved levels list buttons */
@@ -576,7 +1223,9 @@ body {
transform: scale(1.05);
}
-/* Feedback toast animation */
+/* ============================================================================
+ Animations
+ ========================================================================= */
@keyframes slideIn {
from {
transform: translateX(400px);
@@ -588,18 +1237,165 @@ body {
}
}
-/* Touch device improvements */
+/* Respect user motion preferences */
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* ============================================================================
+ Responsive Breakpoints
+ ========================================================================= */
+
+/* Mobile Portrait (320px - 479px) */
+@media (max-width: 479px) {
+ :root {
+ --space-md: 12px;
+ --space-lg: 16px;
+ --space-xl: 24px;
+ --space-2xl: 32px;
+ }
+
+ .header-content {
+ flex-direction: column;
+ gap: var(--space-md);
+ padding: var(--space-md);
+ }
+
+ .header-nav {
+ width: 100%;
+ justify-content: space-between;
+ }
+
+ .app-title {
+ font-size: var(--font-size-base);
+ }
+
+ .nav-link {
+ padding: var(--space-sm) var(--space-md);
+ font-size: var(--font-size-sm);
+ }
+
+ #mainDiv {
+ top: 120px; /* Account for stacked header */
+ padding: 0 var(--space-sm);
+ }
+
+ .card-container {
+ grid-template-columns: 1fr;
+ gap: var(--space-md);
+ padding: 0 var(--space-sm);
+ }
+
+ .level-card {
+ padding: var(--space-lg) var(--space-md);
+ }
+
+ .level-card-title {
+ font-size: var(--font-size-xl);
+ }
+
+ .controls-info {
+ padding: var(--space-md);
+ }
+
+ .controls-grid {
+ grid-template-columns: 1fr;
+ gap: var(--space-md);
+ }
+
+ .login-container,
+ .preloader-content {
+ padding: var(--space-lg);
+ }
+}
+
+/* Mobile Landscape (480px - 767px) */
+@media (min-width: 480px) and (max-width: 767px) {
+ .header-content {
+ padding: var(--space-md) var(--space-lg);
+ }
+
+ .card-container {
+ grid-template-columns: 1fr;
+ gap: var(--space-md);
+ }
+
+ #mainDiv {
+ top: 80px;
+ }
+}
+
+/* Tablet (768px - 1023px) */
+@media (min-width: 768px) and (max-width: 1023px) {
+ .card-container {
+ grid-template-columns: repeat(3, 1fr);
+ gap: var(--space-lg);
+ }
+
+ .level-card-title {
+ font-size: var(--font-size-xl);
+ }
+
+ .controls-grid {
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ }
+}
+
+/* Desktop (1024px+) */
+@media (min-width: 1024px) {
+ .header-content {
+ padding: var(--space-lg) var(--space-2xl);
+ }
+
+ #mainDiv {
+ padding: 0 var(--space-2xl);
+ }
+
+ .card-container {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+
+/* Large Desktop (1440px+) */
+@media (min-width: 1440px) {
+ .header-content,
+ .editor-container {
+ max-width: 1400px;
+ }
+
+ .card-container {
+ max-width: 1400px;
+ }
+}
+
+/* Touch Device Improvements */
@media (hover: none) {
.form-group input[type="text"],
.form-group input[type="number"],
.form-group select {
- padding: 14px;
+ padding: var(--space-md);
font-size: 16px; /* Prevents iOS zoom on focus */
}
.btn-primary,
.btn-success,
- .btn-secondary {
- padding: 16px 24px;
+ .btn-secondary,
+ .level-button,
+ .login-button,
+ .preloader-button {
+ min-height: 44px; /* Minimum touch target size */
+ padding: var(--space-md) var(--space-lg);
+ }
+
+ .nav-link {
+ min-height: 44px;
+ display: flex;
+ align-items: center;
}
}
diff --git a/src/gameConfig.ts b/src/gameConfig.ts
index 43c8264..b6770e7 100644
--- a/src/gameConfig.ts
+++ b/src/gameConfig.ts
@@ -10,7 +10,7 @@ export class GameConfig {
public physicsEnabled: boolean = true;
// Feature flags
- public progressionEnabled: boolean = false; // Set to false for simple rookie level
+ public progressionEnabled: boolean = true; // Enable level progression system
// Ship physics tuning parameters
public shipPhysics = {
@@ -61,7 +61,7 @@ export class GameConfig {
const config = JSON.parse(stored);
this.physicsEnabled = config.physicsEnabled ?? true;
this.debug = config.debug ?? false;
- this.progressionEnabled = config.progressionEnabled ?? false;
+ this.progressionEnabled = config.progressionEnabled ?? true;
// Load ship physics with fallback to defaults
if (config.shipPhysics) {
@@ -86,7 +86,7 @@ export class GameConfig {
public reset(): void {
this.physicsEnabled = true;
this.debug = false;
- this.progressionEnabled = false;
+ this.progressionEnabled = true;
this.shipPhysics = {
maxLinearVelocity: 200,
maxAngularVelocity: 1.4,
diff --git a/src/levelSelector.ts b/src/levelSelector.ts
index be73d7c..4561d7d 100644
--- a/src/levelSelector.ts
+++ b/src/levelSelector.ts
@@ -2,15 +2,26 @@ 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 default levels and custom levels with progression tracking
+ * Shows all 6 default levels in a 3x2 carousel with locked/unlocked states
*/
-export function populateLevelSelector(): boolean {
+export async function populateLevelSelector(): Promise
{
const container = document.getElementById('levelCardsContainer');
if (!container) {
console.warn('Level cards container not found');
@@ -24,24 +35,10 @@ export function populateLevelSelector(): boolean {
if (savedLevels.size === 0) {
container.innerHTML = `
-
-
No Levels Found
-
Something went wrong - default levels should be auto-generated!
-
Go to Level Editor
+
+
No Levels Found
+
Something went wrong - default levels should be auto-generated!
+
Go to Level Editor
`;
return false;
@@ -69,269 +66,162 @@ export function populateLevelSelector(): boolean {
const nextLevel = progression.getNextLevel();
html += `
-
-
Progress
-
+
+
Progress
+
${completedCount} of ${totalCount} default levels completed (${completionPercent.toFixed(0)}%)
-
-
+
- ${nextLevel ? `
Next: ${nextLevel}
` : ''}
+ ${nextLevel ? `
Next: ${nextLevel}
` : ''}
`;
}
- // Default levels section - show all levels if progression disabled, or current/next if enabled
+ // 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) {
- html += `
-
-
Available Levels
-
- `;
-
- // If progression is disabled, just show all default levels
- if (!progressionEnabled) {
- for (const [name, config] of defaultLevels.entries()) {
- const description = config.metadata?.description || `${config.asteroids.length} asteroids โข ${config.planets.length} planets`;
- const estimatedTime = config.metadata?.estimatedTime || '';
+ for (const levelName of DEFAULT_LEVEL_ORDER) {
+ const config = defaultLevels.get(levelName);
+ if (!config) {
+ // Level doesn't exist - show empty slot
html += `
-
-
${name}
-
- Difficulty: ${config.difficulty}${estimatedTime ? ` โข ${estimatedTime}` : ''}
+
+
-
${description}
-
Play Level
+
Level not found
+
This level has not been created yet.
+
Locked
`;
+ continue;
}
- } else {
- // Progression enabled - show current and next level only
- // Get the default level names in order
- const defaultLevelNames = [
- 'Tutorial: Asteroid Field',
- 'Rescue Mission',
- 'Deep Space Patrol',
- 'Enemy Territory',
- 'The Gauntlet',
- 'Final Challenge'
- ];
- // Find current level (last completed or first if none completed)
- let currentLevelName: string | null = null;
- let nextLevelName: string | null = null;
+ const description = config.metadata?.description || `${config.asteroids.length} asteroids โข ${config.planets.length} planets`;
+ const estimatedTime = config.metadata?.estimatedTime || '';
+ const isCompleted = progressionEnabled && progression.isLevelComplete(levelName);
- // Find the first incomplete level (this is the "next" level)
- for (let i = 0; i < defaultLevelNames.length; i++) {
- const levelName = defaultLevelNames[i];
- if (!progression.isLevelComplete(levelName)) {
- nextLevelName = levelName;
- // Current level is the one before (if it exists)
- if (i > 0) {
- currentLevelName = defaultLevelNames[i - 1];
+ // 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 = '
โ
';
+ buttonText = 'Replay';
+ } else if (isCurrentNext && isUnlocked) {
+ cardClasses += ' level-card-current';
+ statusIcon = '
START HERE
';
+ } else if (!isUnlocked) {
+ cardClasses += ' level-card-locked';
+ statusIcon = '
๐
';
+
+ // Determine why it's locked
+ if (!isAuthenticated && !isTutorial(levelName)) {
+ buttonText = 'Sign In Required';
+ lockReason = '
Sign in to unlock
';
+ } else if (progressionEnabled) {
+ const levelIndex = DEFAULT_LEVEL_ORDER.indexOf(levelName);
+ if (levelIndex > 0) {
+ const previousLevel = DEFAULT_LEVEL_ORDER[levelIndex - 1];
+ lockReason = `
Complete "${previousLevel}" to unlock
`;
}
- break;
+ buttonText = 'Locked';
+ } else {
+ buttonText = 'Locked';
}
+ buttonDisabled = ' disabled';
}
- // If all levels complete, show the last level as current
- if (!nextLevelName) {
- currentLevelName = defaultLevelNames[defaultLevelNames.length - 1];
- }
-
- // If no levels completed yet, show first as next (no current)
- if (!currentLevelName && nextLevelName) {
- // First time player - just show the first level
- const config = defaultLevels.get(nextLevelName);
- if (config) {
- const description = config.metadata?.description || `${config.asteroids.length} asteroids โข ${config.planets.length} planets`;
- const estimatedTime = config.metadata?.estimatedTime || '';
-
- html += `
-
-
-
${nextLevelName}
-
START HERE
-
-
- Difficulty: ${config.difficulty}${estimatedTime ? ` โข ${estimatedTime}` : ''}
-
-
${description}
-
Play Level
+ html += `
+
+
- `;
- }
- } else {
- // Show current (completed) level
- if (currentLevelName) {
- const config = defaultLevels.get(currentLevelName);
- if (config) {
- const levelProgress = progression.getLevelProgress(currentLevelName);
- const description = config.metadata?.description || `${config.asteroids.length} asteroids โข ${config.planets.length} planets`;
- const estimatedTime = config.metadata?.estimatedTime || '';
-
- html += `
-
-
-
${currentLevelName}
-
โ
-
-
- Difficulty: ${config.difficulty}${estimatedTime ? ` โข ${estimatedTime}` : ''}
-
-
${description}
- ${levelProgress?.playCount ? `
Played ${levelProgress.playCount} time${levelProgress.playCount > 1 ? 's' : ''}
` : ''}
-
Play Again
-
- `;
- }
- }
-
- // Show next level if it exists
- if (nextLevelName) {
- const config = defaultLevels.get(nextLevelName);
- if (config) {
- const description = config.metadata?.description || `${config.asteroids.length} asteroids โข ${config.planets.length} planets`;
- const estimatedTime = config.metadata?.estimatedTime || '';
-
- html += `
-
-
-
${nextLevelName}
-
NEXT
-
-
- Difficulty: ${config.difficulty}${estimatedTime ? ` โข ${estimatedTime}` : ''}
-
-
${description}
-
Play Level
-
- `;
- }
- }
+
+ Difficulty: ${config.difficulty}${estimatedTime ? ` โข ${estimatedTime}` : ''}
+
+
${description}
+ ${lockReason}
+
${buttonText}
+
+ `;
}
-
- // Show "more levels beyond" indicator if there are additional levels after next
- const nextLevelIndex = defaultLevelNames.indexOf(nextLevelName || '');
- const hasMoreLevels = nextLevelIndex >= 0 && nextLevelIndex < defaultLevelNames.length - 1;
-
- if (hasMoreLevels) {
- const remainingCount = defaultLevelNames.length - nextLevelIndex - 1;
- html += `
-
-
โฆ โฆ โฆ
-
- ${remainingCount} more level${remainingCount > 1 ? 's' : ''} beyond...
-
-
- Complete challenges to unlock new missions
-
-
- `;
- }
- } // End of progressionEnabled else block
}
- // Custom levels section
+ // Show custom levels section if any exist
if (customLevels.size > 0) {
html += `
-
-
Custom Levels
+
+
`;
for (const [name, config] of customLevels.entries()) {
- const timestamp = config.timestamp ? new Date(config.timestamp).toLocaleDateString() : '';
const description = config.metadata?.description || `${config.asteroids.length} asteroids โข ${config.planets.length} planets`;
- const author = config.metadata?.author || 'Unknown';
+ const author = config.metadata?.author ? ` by ${config.metadata.author}` : '';
html += `
-
${name}
-
- Difficulty: ${config.difficulty} โข By ${author}
+
-
${description}
- ${timestamp ? `
Created ${timestamp}
` : ''}
+
+ Custom${author} โข ${config.difficulty}
+
+
${description}
Play Level
`;
}
}
- // Editor unlock button (always unlocked if progression disabled)
- const isEditorUnlocked = !progressionEnabled || progression.isEditorUnlocked();
- const completedCount = progression.getCompletedCount();
-
- html += `
-
- ${isEditorUnlocked ? `
-
- ๐จ Create Custom Level
-
- ` : `
-
- ๐ Level Editor (Complete ${3 - completedCount} more level${(3 - completedCount) !== 1 ? 's' : ''})
-
- `}
-
- `;
-
container.innerHTML = html;
- // Add event listeners to level buttons
- container.querySelectorAll('.level-button').forEach(button => {
+ // Attach event listeners to all level buttons
+ const buttons = container.querySelectorAll('.level-button:not([disabled])');
+ buttons.forEach(button => {
button.addEventListener('click', (e) => {
- const levelName = (e.target as HTMLButtonElement).dataset.level;
+ const target = e.target as HTMLButtonElement;
+ const levelName = target.getAttribute('data-level');
if (levelName) {
selectLevel(levelName);
}
@@ -342,71 +232,32 @@ export function populateLevelSelector(): boolean {
}
/**
- * Initialize level button listeners (for any dynamically created buttons)
- */
-export function initializeLevelButtons(): void {
- document.querySelectorAll('.level-button').forEach(button => {
- if (!button.hasAttribute('data-listener-attached')) {
- button.setAttribute('data-listener-attached', 'true');
- button.addEventListener('click', (e) => {
- const levelName = (e.target as HTMLButtonElement).dataset.level;
- if (levelName) {
- selectLevel(levelName);
- }
- });
- }
- });
-}
-
-/**
- * Select a level and store it for Level1 to use
+ * 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 "${levelName}" not found`);
- alert(`Level "${levelName}" not found!`);
+ console.error(`Level not found: ${levelName}`);
return;
}
- // Store selected level name
- sessionStorage.setItem(SELECTED_LEVEL_KEY, levelName);
+ // Save selected level
+ localStorage.setItem(SELECTED_LEVEL_KEY, levelName);
- debugLog(`Selected level: ${levelName}`);
-
- // Trigger level start (the existing code will pick this up)
- const event = new CustomEvent('levelSelected', { detail: { levelName, config } });
+ // Dispatch custom event that Main class will listen for
+ const event = new CustomEvent('levelSelected', {
+ detail: { levelName, config }
+ });
window.dispatchEvent(event);
}
/**
- * Get the currently selected level configuration
+ * Get the last selected level name
*/
-export function getSelectedLevel(): { name: string, config: LevelConfig } | null {
- const levelName = sessionStorage.getItem(SELECTED_LEVEL_KEY);
- if (!levelName) return null;
-
- const savedLevels = getSavedLevels();
- const config = savedLevels.get(levelName);
-
- if (!config) return null;
-
- return { name: levelName, config };
-}
-
-/**
- * Clear the selected level
- */
-export function clearSelectedLevel(): void {
- sessionStorage.removeItem(SELECTED_LEVEL_KEY);
-}
-
-/**
- * Check if there are any saved levels
- */
-export function hasSavedLevels(): boolean {
- const savedLevels = getSavedLevels();
- return savedLevels.size > 0;
+export function getSelectedLevel(): string | null {
+ return localStorage.getItem(SELECTED_LEVEL_KEY);
}
diff --git a/src/loginScreen.ts b/src/loginScreen.ts
index 891b8f9..197535f 100644
--- a/src/loginScreen.ts
+++ b/src/loginScreen.ts
@@ -12,65 +12,19 @@ export function showLoginScreen(): void {
}
container.innerHTML = `
-
-
-
- Space Combat VR
-
+
+
+
Space Combat VR
-
+
Welcome, pilot! Authentication required to access your mission data and track your progress across the galaxy.
-
+
Log In / Sign Up
-
+
Secured by Auth0
@@ -100,22 +54,12 @@ export function updateUserProfile(username: string | null): void {
if (username) {
// User is authenticated - show profile and logout
+ profileContainer.className = 'user-profile';
profileContainer.innerHTML = `
-
+
Welcome, ${username}
-
+
Log Out
`;
@@ -129,21 +73,9 @@ export function updateUserProfile(username: string | null): void {
}
} else {
// User not authenticated - show login/signup button
+ profileContainer.className = '';
profileContainer.innerHTML = `
-
+
Sign Up / Log In
`;
diff --git a/src/main.ts b/src/main.ts
index a8a7ee0..08ae428 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -23,7 +23,7 @@ import setLoadingMessage from "./setLoadingMessage";
import {RockFactory} from "./rockFactory";
import {ControllerDebug} from "./controllerDebug";
import {router, showView} from "./router";
-import {hasSavedLevels, populateLevelSelector} from "./levelSelector";
+import {populateLevelSelector} from "./levelSelector";
import {LevelConfig} from "./levelConfig";
import {generateDefaultLevels} from "./levelEditor";
import debugLog from './debug';
@@ -64,17 +64,20 @@ export class Main {
// Hide all UI elements
const mainDiv = document.querySelector('#mainDiv');
const levelSelect = document.querySelector('#levelSelect') as HTMLElement;
- const editorLink = document.querySelector('.editor-link') as HTMLElement;
- const settingsLink = document.querySelector('.settings-link') as HTMLElement;
+ const appHeader = document.querySelector('#appHeader') as HTMLElement;
if (levelSelect) {
levelSelect.style.display = 'none';
}
- if (editorLink) {
- editorLink.style.display = 'none';
+ if (appHeader) {
+ appHeader.style.display = 'none';
}
- if (settingsLink) {
- settingsLink.style.display = 'none';
+
+ // Hide Discord widget during gameplay
+ const discord = (window as any).__discordWidget as DiscordWidget;
+ if (discord) {
+ debugLog('[Main] Hiding Discord widget for gameplay');
+ discord.hide();
}
// Show preloader for initialization
@@ -227,8 +230,7 @@ export class Main {
// Hide all UI elements
const mainDiv = document.querySelector('#mainDiv');
const levelSelect = document.querySelector('#levelSelect') as HTMLElement;
- const editorLink = document.querySelector('.editor-link') as HTMLElement;
- const settingsLink = document.querySelector('.settings-link') as HTMLElement;
+ const appHeader = document.querySelector('#appHeader') as HTMLElement;
debugLog('[Main] mainDiv exists:', !!mainDiv);
debugLog('[Main] levelSelect exists:', !!levelSelect);
@@ -237,11 +239,8 @@ export class Main {
levelSelect.style.display = 'none';
debugLog('[Main] levelSelect hidden');
}
- if (editorLink) {
- editorLink.style.display = 'none';
- }
- if (settingsLink) {
- settingsLink.style.display = 'none';
+ if (appHeader) {
+ appHeader.style.display = 'none';
}
setLoadingMessage("Initializing Test Scene...");
@@ -312,17 +311,13 @@ export class Main {
// Hide main menu
const levelSelect = document.querySelector('#levelSelect') as HTMLElement;
- const editorLink = document.querySelector('.editor-link') as HTMLElement;
- const settingsLink = document.querySelector('.settings-link') as HTMLElement;
+ const appHeader = document.querySelector('#appHeader') as HTMLElement;
if (levelSelect) {
levelSelect.style.display = 'none';
}
- if (editorLink) {
- editorLink.style.display = 'none';
- }
- if (settingsLink) {
- settingsLink.style.display = 'none';
+ if (appHeader) {
+ appHeader.style.display = 'none';
}
// Show replay selection screen
@@ -342,11 +337,9 @@ export class Main {
if (levelSelect) {
levelSelect.style.display = 'block';
}
- if (editorLink) {
- editorLink.style.display = 'block';
- }
- if (settingsLink) {
- settingsLink.style.display = 'block';
+ const appHeader = document.querySelector('#appHeader') as HTMLElement;
+ if (appHeader) {
+ appHeader.style.display = 'block';
}
}
);
@@ -364,11 +357,9 @@ export class Main {
if (levelSelect) {
levelSelect.style.display = 'block';
}
- if (editorLink) {
- editorLink.style.display = 'block';
- }
- if (settingsLink) {
- settingsLink.style.display = 'block';
+ const appHeader = document.querySelector('#appHeader') as HTMLElement;
+ if (appHeader) {
+ appHeader.style.display = 'block';
}
}
);
@@ -610,10 +601,16 @@ router.on('/', async () => {
updateUserProfile(null);
}
+ // Show the app header
+ const appHeader = document.getElementById('appHeader');
+ if (appHeader) {
+ appHeader.style.display = 'block';
+ }
+
// Just show the level selector - don't initialize anything yet!
if (!DEBUG_CONTROLLERS) {
debugLog('[Router] Populating level selector (no engine initialization yet)');
- populateLevelSelector();
+ await populateLevelSelector();
// Create Main instance lazily only if it doesn't exist
// But don't initialize it yet - that will happen on level selection
@@ -626,7 +623,9 @@ router.on('/', async () => {
const demo = new Demo(main);
}
- // Initialize Discord widget (if not already initialized)
+ // Discord widget initialization - DISABLED FOR NOW
+ // Uncomment to enable Discord chat widget
+ /*
if (!(window as any).__discordWidget) {
debugLog('[Router] Initializing Discord widget');
const discord = new DiscordWidget();
@@ -645,6 +644,7 @@ router.on('/', async () => {
console.error('[Router] Failed to initialize Discord widget:', error);
});
}
+ */
}
debugLog('[Router] Home route handler complete');
@@ -674,6 +674,23 @@ router.on('/settings', () => {
// Generate default levels if localStorage is empty
generateDefaultLevels();
+// Suppress non-critical BabylonJS shader loading errors during development
+// Note: After Vite config fix to pre-bundle shaders, these errors should no longer occur
+// Keeping this handler for backwards compatibility with older cached builds
+window.addEventListener('unhandledrejection', (event) => {
+ const error = event.reason;
+ if (error && error.message) {
+ // Only suppress specific shader-related errors, not asset loading errors
+ if (error.message.includes('rgbdDecode.fragment') ||
+ error.message.includes('procedural.vertex') ||
+ (error.message.includes('Failed to fetch dynamically imported module') &&
+ (error.message.includes('rgbdDecode') || error.message.includes('procedural')))) {
+ debugLog('[Main] Suppressed shader loading error (should be fixed by Vite pre-bundling):', error.message);
+ event.preventDefault(); // Prevent error from appearing in console
+ }
+ }
+});
+
// Start the router after all routes are registered
router.start();
diff --git a/src/preloader.ts b/src/preloader.ts
index ce4029d..91614e8 100644
--- a/src/preloader.ts
+++ b/src/preloader.ts
@@ -19,88 +19,27 @@ export class Preloader {
// Create preloader container
this.container = document.createElement('div');
- this.container.id = 'preloader';
- this.container.style.cssText = `
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- background: rgba(0, 0, 0, 0.95);
- z-index: 10000;
- padding: 20px;
- `;
+ this.container.className = 'preloader';
this.container.innerHTML = `
-
-
+
+
๐ Space Combat VR
-
+
Initializing...
-
-
+
-
+
Start Game
-
+
Initializing game engine... Assets will load when you select a level.
diff --git a/src/progression.ts b/src/progression.ts
index bc75997..1d33727 100644
--- a/src/progression.ts
+++ b/src/progression.ts
@@ -186,7 +186,7 @@ export class ProgressionManager {
*/
private getDefaultLevelNames(): string[] {
return [
- 'Tutorial: Asteroid Field',
+ 'Rookie Training',
'Rescue Mission',
'Deep Space Patrol',
'Enemy Territory',
@@ -236,6 +236,29 @@ export class ProgressionManager {
return total > 0 ? (completed / total) * 100 : 0;
}
+ /**
+ * Check if a level is unlocked and can be played
+ * Tutorial is always unlocked, other levels require previous level completion
+ */
+ public isLevelUnlocked(levelName: string): boolean {
+ const defaultLevels = this.getDefaultLevelNames();
+ const levelIndex = defaultLevels.indexOf(levelName);
+
+ // If not a default level (custom level), it's always unlocked
+ if (levelIndex === -1) {
+ return true;
+ }
+
+ // First level (Tutorial) is always unlocked
+ if (levelIndex === 0) {
+ return true;
+ }
+
+ // Other levels require previous level to be completed
+ const previousLevel = defaultLevels[levelIndex - 1];
+ return this.isLevelComplete(previousLevel);
+ }
+
/**
* Reset all progression (for testing or user request)
*/
diff --git a/src/utils/loadAsset.ts b/src/utils/loadAsset.ts
index 861d1e0..39b431d 100644
--- a/src/utils/loadAsset.ts
+++ b/src/utils/loadAsset.ts
@@ -1,20 +1,51 @@
import {DefaultScene} from "../defaultScene";
import {AbstractMesh, AssetContainer, LoadAssetContainerAsync} from "@babylonjs/core";
+import debugLog from "../debug";
export type LoadedAsset = {
container: AssetContainer,
meshes: Map,
}
export default async function loadAsset(file: string, theme: string = "default"): Promise {
- const container = await LoadAssetContainerAsync(`assets/themes/${theme}/models/${file}`, DefaultScene.MainScene);
- const map: Map = new Map();
- container.addAllToScene();
- for (const mesh of container.rootNodes[0].getChildMeshes(false)) {
- console.log(mesh.id, mesh);
- //mesh.setParent(null);
- //mesh.rotation.y = Math.PI /2;
- //mesh.rotation.z = Math.PI;
- map.set(mesh.id, mesh);
+ const assetPath = `assets/themes/${theme}/models/${file}`;
+ debugLog(`[loadAsset] Loading: ${assetPath}`);
+
+ try {
+ const container = await LoadAssetContainerAsync(assetPath, DefaultScene.MainScene);
+ debugLog(`[loadAsset] โ Container loaded for ${file}`);
+
+ const map: Map = new Map();
+ container.addAllToScene();
+
+ debugLog(`[loadAsset] Root nodes count: ${container.rootNodes.length}`);
+ if (container.rootNodes.length === 0) {
+ console.error(`[loadAsset] ERROR: No root nodes found in ${file}`);
+ return {container: container, meshes: map};
+ }
+
+ for (const mesh of container.rootNodes[0].getChildMeshes(false)) {
+ console.log(mesh.id, mesh);
+ // Ensure mesh is visible and enabled
+ mesh.isVisible = true;
+ mesh.setEnabled(true);
+
+ // Fix emissive materials to work without lighting
+ if (mesh.material) {
+ const material = mesh.material as any;
+
+ // Disable lighting on materials so emissive works without light sources
+ if (material.disableLighting !== undefined) {
+ material.disableLighting = true;
+ }
+ }
+
+ map.set(mesh.id, mesh);
+ }
+
+ debugLog(`[loadAsset] โ Loaded ${map.size} meshes from ${file}`);
+ return {container: container, meshes: map};
+ } catch (error) {
+ console.error(`[loadAsset] FAILED to load ${assetPath}:`, error);
+ throw error;
}
- return {container: container, meshes: map};
}
\ No newline at end of file
diff --git a/styles.css b/styles.css
deleted file mode 100644
index 4c85802..0000000
--- a/styles.css
+++ /dev/null
@@ -1,91 +0,0 @@
-#levelSelect {
- text-align: center;
- padding: 20px;
- opacity: 0;
- transition: opacity 0.5s ease-in;
-}
-
-#levelSelect.ready {
- opacity: 1;
-}
-
-#levelSelect h1 {
- color: #fff;
- font-size: 2.5em;
- margin-bottom: 30px;
- text-shadow: 0 0 10px rgba(0, 150, 255, 0.8);
-}
-
-.card-container {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 20px;
- max-width: 1200px;
- margin: 0 auto;
- padding: 20px;
-}
-
-.level-card {
- background: rgba(20, 20, 40, 0.9);
- border: 2px solid rgba(0, 150, 255, 0.5);
- border-radius: 12px;
- padding: 30px 20px;
- transition: all 0.3s ease;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
-}
-
-.level-card:hover {
- transform: translateY(-5px);
- border-color: rgba(0, 200, 255, 0.8);
- box-shadow: 0 8px 16px rgba(0, 150, 255, 0.4);
-}
-
-.level-card h2 {
- color: #00d4ff;
- font-size: 1.8em;
- margin-bottom: 15px;
- text-transform: uppercase;
-}
-
-.level-card p {
- color: #ccc;
- font-size: 1em;
- line-height: 1.6;
- margin-bottom: 20px;
- min-height: 50px;
-}
-
-.level-button {
- background: linear-gradient(135deg, #0066cc, #0099ff);
- color: white;
- border: none;
- padding: 12px 30px;
- font-size: 1.1em;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.3s ease;
- font-weight: bold;
- text-transform: uppercase;
- letter-spacing: 1px;
-}
-
-.level-button:hover {
- background: linear-gradient(135deg, #0088ff, #00bbff);
- box-shadow: 0 4px 12px rgba(0, 150, 255, 0.6);
- transform: scale(1.05);
-}
-
-.level-button:active {
- transform: scale(0.98);
-}
-
-@media (max-width: 768px) {
- .card-container {
- grid-template-columns: 1fr;
- gap: 15px;
- }
-
- #levelSelect h1 {
- font-size: 2em;
- }
-}
diff --git a/vite.config.ts b/vite.config.ts
index 9cb70cd..b638463 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -9,7 +9,8 @@ export default defineConfig({
rollupOptions: {
output: {
manualChunks: {
- 'babylon': ['@babylonjs/core']
+ 'babylon': ['@babylonjs/core'],
+ 'babylon-procedural': ['@babylonjs/procedural-textures']
}
}
}
@@ -19,7 +20,19 @@ export default defineConfig({
define: {
global: 'window',
}
- }
+ },
+ // Include BabylonJS modules - force pre-bundle to prevent dynamic import issues
+ include: [
+ '@babylonjs/core',
+ '@babylonjs/loaders',
+ '@babylonjs/havok',
+ '@babylonjs/procedural-textures',
+ '@babylonjs/procedural-textures/fireProceduralTexture'
+ ],
+ // Prevent cache invalidation issues with CloudFlare proxy
+ force: false,
+ // Exclude patterns that trigger unnecessary re-optimization
+ exclude: []
},
server: {
port: 3000,