Simplify level system and add asteroid-mania level
All checks were successful
Build / build (push) Successful in 1m29s
All checks were successful
Build / build (push) Successful in 1m29s
- Add new asteroid-mania level to directory and DEFAULT_LEVEL_ORDER - Remove level caching entirely (always fetch fresh from network) - Delete legacy router.ts, levelSelector.ts, and levelVersionManager.ts - Remove unused router handlers from main.ts (~120 lines) - Fix projectile curving by cloning velocity vector in weaponSystem.ts - Update LevelSelect.svelte to include asteroid-mania 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3a5cf3e074
commit
3ff1ffeb45
357
CLOUD_STORAGE_PLAN.md
Normal file
357
CLOUD_STORAGE_PLAN.md
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
# Cloud Leaderboard Implementation Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Implement a global cloud-based leaderboard using **Supabase** with existing **Auth0** authentication. No backend server required - uses direct client-to-database communication with Row Level Security.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌─────────┐ ┌───────────┐
|
||||||
|
│ Browser │────▶│ Auth0 │────▶│ JWT Token │
|
||||||
|
│ (Game) │ └─────────┘ └─────┬─────┘
|
||||||
|
│ │ │
|
||||||
|
│ Supabase │◀──────────────────────────┘
|
||||||
|
│ JS Client │
|
||||||
|
│ │ ┌───────────────────────────┐
|
||||||
|
│ │────▶│ Supabase (Postgres + RLS)│
|
||||||
|
└─────────────┘ │ - leaderboard table │
|
||||||
|
│ - Row Level Security │
|
||||||
|
└───────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
### Phase 1: Supabase Project Setup (Manual - Dashboard)
|
||||||
|
|
||||||
|
1. Create free Supabase project at https://supabase.com
|
||||||
|
2. Get project credentials:
|
||||||
|
- `SUPABASE_URL` (e.g., `https://xxx.supabase.co`)
|
||||||
|
- `SUPABASE_ANON_KEY` (public, safe for client)
|
||||||
|
3. Configure Third-Party Auth for Auth0:
|
||||||
|
- Navigate to Authentication → Third-Party Auth
|
||||||
|
- Add Auth0 integration with your tenant ID
|
||||||
|
4. Create `leaderboard` table:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE leaderboard (
|
||||||
|
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL, -- Auth0 sub claim
|
||||||
|
player_name TEXT NOT NULL,
|
||||||
|
level_id TEXT NOT NULL,
|
||||||
|
level_name TEXT NOT NULL,
|
||||||
|
completed BOOLEAN NOT NULL,
|
||||||
|
end_reason TEXT NOT NULL, -- 'victory' | 'death' | 'stranded'
|
||||||
|
|
||||||
|
-- Stats
|
||||||
|
game_time_seconds NUMERIC NOT NULL,
|
||||||
|
asteroids_destroyed INTEGER NOT NULL,
|
||||||
|
total_asteroids INTEGER NOT NULL,
|
||||||
|
accuracy NUMERIC NOT NULL,
|
||||||
|
hull_damage_taken NUMERIC NOT NULL,
|
||||||
|
fuel_consumed NUMERIC NOT NULL,
|
||||||
|
|
||||||
|
-- Scoring
|
||||||
|
final_score INTEGER NOT NULL,
|
||||||
|
star_rating INTEGER NOT NULL, -- 0-12
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for leaderboard queries
|
||||||
|
CREATE INDEX idx_leaderboard_score ON leaderboard(final_score DESC);
|
||||||
|
CREATE INDEX idx_leaderboard_user ON leaderboard(user_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Enable Row Level Security:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Enable RLS
|
||||||
|
ALTER TABLE leaderboard ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Anyone can read leaderboard (global leaderboard)
|
||||||
|
CREATE POLICY "Anyone can read leaderboard" ON leaderboard
|
||||||
|
FOR SELECT USING (true);
|
||||||
|
|
||||||
|
-- Authenticated users can insert their own scores
|
||||||
|
CREATE POLICY "Users can insert own scores" ON leaderboard
|
||||||
|
FOR INSERT WITH CHECK (
|
||||||
|
auth.jwt() ->> 'sub' = user_id
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Auth0 Action Configuration (Manual - Auth0 Dashboard)
|
||||||
|
|
||||||
|
Create a Post-Login Action to add required claims to JWT:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Auth0 Action: Add Supabase Claims
|
||||||
|
exports.onExecutePostLogin = async (event, api) => {
|
||||||
|
const namespace = 'https://supabase.com/';
|
||||||
|
|
||||||
|
// Add the 'authenticated' role claim for Supabase RLS
|
||||||
|
api.accessToken.setCustomClaim(`${namespace}role`, 'authenticated');
|
||||||
|
|
||||||
|
// Supabase expects 'sub' claim which Auth0 already provides
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Install Supabase Client
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @supabase/supabase-js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: Create Supabase Service
|
||||||
|
|
||||||
|
**New file: `src/services/supabaseService.ts`**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
import { AuthService } from './authService';
|
||||||
|
|
||||||
|
const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL;
|
||||||
|
const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||||
|
|
||||||
|
export class SupabaseService {
|
||||||
|
private static instance: SupabaseService;
|
||||||
|
private client: SupabaseClient;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): SupabaseService {
|
||||||
|
if (!SupabaseService.instance) {
|
||||||
|
SupabaseService.instance = new SupabaseService();
|
||||||
|
}
|
||||||
|
return SupabaseService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update client with Auth0 token for authenticated requests
|
||||||
|
async setAuthToken(): Promise<void> {
|
||||||
|
const authService = AuthService.getInstance();
|
||||||
|
const token = await authService.getAccessToken();
|
||||||
|
if (token) {
|
||||||
|
this.client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||||
|
global: {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient(): SupabaseClient {
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 5: Create Cloud Leaderboard Service
|
||||||
|
|
||||||
|
**New file: `src/services/cloudLeaderboardService.ts`**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { SupabaseService } from './supabaseService';
|
||||||
|
import { AuthService } from './authService';
|
||||||
|
import type { GameResult } from './gameResultsService';
|
||||||
|
|
||||||
|
export interface CloudLeaderboardEntry {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
player_name: string;
|
||||||
|
level_id: string;
|
||||||
|
level_name: string;
|
||||||
|
completed: boolean;
|
||||||
|
end_reason: string;
|
||||||
|
game_time_seconds: number;
|
||||||
|
asteroids_destroyed: number;
|
||||||
|
total_asteroids: number;
|
||||||
|
accuracy: number;
|
||||||
|
hull_damage_taken: number;
|
||||||
|
fuel_consumed: number;
|
||||||
|
final_score: number;
|
||||||
|
star_rating: number;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CloudLeaderboardService {
|
||||||
|
private static instance: CloudLeaderboardService;
|
||||||
|
|
||||||
|
static getInstance(): CloudLeaderboardService {
|
||||||
|
if (!CloudLeaderboardService.instance) {
|
||||||
|
CloudLeaderboardService.instance = new CloudLeaderboardService();
|
||||||
|
}
|
||||||
|
return CloudLeaderboardService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit a score to cloud leaderboard
|
||||||
|
async submitScore(result: GameResult): Promise<boolean> {
|
||||||
|
const supabase = SupabaseService.getInstance();
|
||||||
|
await supabase.setAuthToken();
|
||||||
|
|
||||||
|
const authService = AuthService.getInstance();
|
||||||
|
const user = authService.getUser();
|
||||||
|
|
||||||
|
if (!user?.sub) {
|
||||||
|
console.warn('Cannot submit score: user not authenticated');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
user_id: user.sub,
|
||||||
|
player_name: result.playerName,
|
||||||
|
level_id: result.levelId,
|
||||||
|
level_name: result.levelName,
|
||||||
|
completed: result.completed,
|
||||||
|
end_reason: result.endReason,
|
||||||
|
game_time_seconds: result.gameTimeSeconds,
|
||||||
|
asteroids_destroyed: result.asteroidsDestroyed,
|
||||||
|
total_asteroids: result.totalAsteroids,
|
||||||
|
accuracy: result.accuracy,
|
||||||
|
hull_damage_taken: result.hullDamageTaken,
|
||||||
|
fuel_consumed: result.fuelConsumed,
|
||||||
|
final_score: result.finalScore,
|
||||||
|
star_rating: result.starRating
|
||||||
|
};
|
||||||
|
|
||||||
|
const { error } = await supabase.getClient()
|
||||||
|
.from('leaderboard')
|
||||||
|
.insert(entry);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to submit score:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch global leaderboard (top scores)
|
||||||
|
async getGlobalLeaderboard(limit = 20): Promise<CloudLeaderboardEntry[]> {
|
||||||
|
const supabase = SupabaseService.getInstance();
|
||||||
|
|
||||||
|
const { data, error } = await supabase.getClient()
|
||||||
|
.from('leaderboard')
|
||||||
|
.select('*')
|
||||||
|
.order('final_score', { ascending: false })
|
||||||
|
.limit(limit);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch leaderboard:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's personal best scores
|
||||||
|
async getUserScores(userId: string, limit = 10): Promise<CloudLeaderboardEntry[]> {
|
||||||
|
const supabase = SupabaseService.getInstance();
|
||||||
|
|
||||||
|
const { data, error } = await supabase.getClient()
|
||||||
|
.from('leaderboard')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.order('final_score', { ascending: false })
|
||||||
|
.limit(limit);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch user scores:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 6: Integrate with Existing Code
|
||||||
|
|
||||||
|
**Modify: `src/services/gameResultsService.ts`**
|
||||||
|
|
||||||
|
Add cloud submission after local save:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { CloudLeaderboardService } from './cloudLeaderboardService';
|
||||||
|
|
||||||
|
// In saveResult() method, after localStorage save:
|
||||||
|
async saveResult(result: GameResult): Promise<void> {
|
||||||
|
// Existing localStorage save
|
||||||
|
const results = this.getAllResults();
|
||||||
|
results.push(result);
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(results));
|
||||||
|
|
||||||
|
// NEW: Submit to cloud leaderboard
|
||||||
|
try {
|
||||||
|
const cloudService = CloudLeaderboardService.getInstance();
|
||||||
|
await cloudService.submitScore(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Cloud leaderboard submission failed:', error);
|
||||||
|
// Don't block on cloud failure - local save succeeded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Modify: `src/components/leaderboard/Leaderboard.svelte`**
|
||||||
|
|
||||||
|
Add toggle between local and cloud leaderboard:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
import { CloudLeaderboardService } from '../../services/cloudLeaderboardService';
|
||||||
|
|
||||||
|
let showCloud = true;
|
||||||
|
let cloudResults = [];
|
||||||
|
|
||||||
|
async function loadCloudLeaderboard() {
|
||||||
|
const service = CloudLeaderboardService.getInstance();
|
||||||
|
cloudResults = await service.getGlobalLeaderboard(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (showCloud) loadCloudLeaderboard();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Add toggle UI and display cloudResults when showCloud is true -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 7: Environment Variables
|
||||||
|
|
||||||
|
**Create/update: `.env`**
|
||||||
|
|
||||||
|
```
|
||||||
|
VITE_SUPABASE_URL=https://your-project.supabase.co
|
||||||
|
VITE_SUPABASE_ANON_KEY=your-anon-key
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files to Modify
|
||||||
|
|
||||||
|
| File | Action | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `src/services/supabaseService.ts` | CREATE | Supabase client singleton |
|
||||||
|
| `src/services/cloudLeaderboardService.ts` | CREATE | Cloud leaderboard API |
|
||||||
|
| `src/services/gameResultsService.ts` | MODIFY | Add cloud submission |
|
||||||
|
| `src/components/leaderboard/Leaderboard.svelte` | MODIFY | Add cloud data source |
|
||||||
|
| `.env` | CREATE/MODIFY | Add Supabase credentials |
|
||||||
|
| `package.json` | MODIFY | Add @supabase/supabase-js |
|
||||||
|
|
||||||
|
## External Setup Required (Manual)
|
||||||
|
|
||||||
|
1. **Supabase Dashboard**: Create project, table, RLS policies, Auth0 integration
|
||||||
|
2. **Auth0 Dashboard**: Create Post-Login Action for role claim
|
||||||
|
|
||||||
|
## Cost Analysis
|
||||||
|
|
||||||
|
- **Supabase Free Tier**: 500MB database, 10k MAU, 1GB file storage
|
||||||
|
- **Your usage** (<100 players): Well within free tier
|
||||||
|
- **Cost**: $0/month
|
||||||
|
|
||||||
|
## Considerations
|
||||||
|
|
||||||
|
- **Offline/failure handling**: Falls back to localStorage if cloud fails
|
||||||
|
- **Inactivity pause**: Free tier projects pause after 7 days inactivity (easy to unpause)
|
||||||
|
- **No anti-cheat**: Scores submitted directly from client (per your requirements)
|
||||||
289
public/levels/asteroid-mania.json
Normal file
289
public/levels/asteroid-mania.json
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
{
|
||||||
|
"version": "1.2",
|
||||||
|
"difficulty": "rookie",
|
||||||
|
"timestamp": "2025-11-11T23:44:24.810Z",
|
||||||
|
"metadata": {
|
||||||
|
"author": "System",
|
||||||
|
"description": "Asteroid Mania!",
|
||||||
|
"estimatedTime": "5-8 minutes",
|
||||||
|
"type": "default"
|
||||||
|
},
|
||||||
|
"ship": {
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"rotation": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"linearVelocity": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"angularVelocity": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"startBase": {
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"baseGlbPath": "base.glb"
|
||||||
|
},
|
||||||
|
"sun": {
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
400
|
||||||
|
],
|
||||||
|
"diameter": 50,
|
||||||
|
"intensity": 1000000
|
||||||
|
},
|
||||||
|
"planets": [],
|
||||||
|
"asteroids": [
|
||||||
|
{
|
||||||
|
"id": "asteroid-0",
|
||||||
|
"position": [
|
||||||
|
242.60734209985543,
|
||||||
|
-114.56996058926651,
|
||||||
|
5.575229357062
|
||||||
|
],
|
||||||
|
"scale": 2,
|
||||||
|
"linearVelocity": [
|
||||||
|
-170.167175139332553,
|
||||||
|
80.177863609194048,
|
||||||
|
-0.39450965492725215
|
||||||
|
],
|
||||||
|
"angularVelocity": [
|
||||||
|
-0.834980024785148,
|
||||||
|
0.9648009938830251,
|
||||||
|
0.8185653748494373
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "asteroid-1",
|
||||||
|
"position": [
|
||||||
|
145.90971366777896,
|
||||||
|
42.273817290099984,
|
||||||
|
-244.80503221456152
|
||||||
|
],
|
||||||
|
"scale": 6,
|
||||||
|
"linearVelocity": [
|
||||||
|
-14.737555578618144,
|
||||||
|
-42.168846343154079,
|
||||||
|
240.72643991613985
|
||||||
|
],
|
||||||
|
"angularVelocity": [
|
||||||
|
0.575649251710729,
|
||||||
|
-2.8551046445434349,
|
||||||
|
-0.9477761112717422
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "asteroid-2",
|
||||||
|
"position": [
|
||||||
|
195.05992969157123,
|
||||||
|
-311.0584087077698,
|
||||||
|
-22.40662780090249
|
||||||
|
],
|
||||||
|
"scale": 4,
|
||||||
|
"linearVelocity": [
|
||||||
|
-160.81570103491442,
|
||||||
|
9.660316715266058,
|
||||||
|
160.9316276535952197
|
||||||
|
],
|
||||||
|
"angularVelocity": [
|
||||||
|
0.8587973467645904,
|
||||||
|
0.25620436829463733,
|
||||||
|
-0.7705721105608303
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "asteroid-3",
|
||||||
|
"position": [
|
||||||
|
-0.9357515100775112,
|
||||||
|
85.76554222686204,
|
||||||
|
249.4670613777975
|
||||||
|
],
|
||||||
|
"scale": 17.34408913479813,
|
||||||
|
"linearVelocity": [
|
||||||
|
0.07109432360434195,
|
||||||
|
-6.440116659897093,
|
||||||
|
-18.953420645560346
|
||||||
|
],
|
||||||
|
"angularVelocity": [
|
||||||
|
0.19650221972006143,
|
||||||
|
0.4226089665809898,
|
||||||
|
-0.9419176203015098
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "asteroid-4",
|
||||||
|
"position": [
|
||||||
|
-254.14456477364413,
|
||||||
|
54.65967750105119,
|
||||||
|
82.65652287437858
|
||||||
|
],
|
||||||
|
"scale": 4,
|
||||||
|
"linearVelocity": [
|
||||||
|
22.372081486064396,
|
||||||
|
-400.723605553550473,
|
||||||
|
-7.2761676675924445
|
||||||
|
],
|
||||||
|
"angularVelocity": [
|
||||||
|
-0.22039903827783025,
|
||||||
|
0.03062354927084643,
|
||||||
|
0.3628209366655213
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "asteroid-5",
|
||||||
|
"position": [
|
||||||
|
-257.7249224576784,
|
||||||
|
-112.97325792551102,
|
||||||
|
-92.25372143357285
|
||||||
|
],
|
||||||
|
"scale": 12,
|
||||||
|
"linearVelocity": [
|
||||||
|
17.764361846647077,
|
||||||
|
7.855903788127005,
|
||||||
|
6.358828139777149
|
||||||
|
],
|
||||||
|
"angularVelocity": [
|
||||||
|
-0.27982741337355455,
|
||||||
|
0.2465507084870353,
|
||||||
|
-0.8489416083688623
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "asteroid-6",
|
||||||
|
"position": [
|
||||||
|
-61.74000302102928,
|
||||||
|
103.75532261403117,
|
||||||
|
-224.6843746923246
|
||||||
|
],
|
||||||
|
"scale": 14.438006716048399,
|
||||||
|
"linearVelocity": [
|
||||||
|
4.573571795825104,
|
||||||
|
-7.611901885044768,
|
||||||
|
16.644154013167135
|
||||||
|
],
|
||||||
|
"angularVelocity": [
|
||||||
|
-0.41949593751738457,
|
||||||
|
-0.5881266007071146,
|
||||||
|
0.2671577602439994
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "asteroid-7",
|
||||||
|
"position": [
|
||||||
|
16.846663100767792,
|
||||||
|
72.36836836065181,
|
||||||
|
-271.36235273889974
|
||||||
|
],
|
||||||
|
"scale": 10,
|
||||||
|
"linearVelocity": [
|
||||||
|
220.2776861733199087,
|
||||||
|
-345.412726361379603,
|
||||||
|
-20.580688530433683
|
||||||
|
],
|
||||||
|
"angularVelocity": [
|
||||||
|
-0.5793176374486806,
|
||||||
|
0.8207961833131412,
|
||||||
|
-0.034658037798875885
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "asteroid-8",
|
||||||
|
"position": [
|
||||||
|
129.11110725214024,
|
||||||
|
91.10691458736655,
|
||||||
|
205.0668479159754
|
||||||
|
],
|
||||||
|
"scale": 10,
|
||||||
|
"linearVelocity": [
|
||||||
|
-10.330594112594069,
|
||||||
|
-7.209743461671342,
|
||||||
|
160.4080567261488
|
||||||
|
],
|
||||||
|
"angularVelocity": [
|
||||||
|
-2.572098306083443,
|
||||||
|
0.6581860817605101,
|
||||||
|
-0.7141435682550208
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "asteroid-9",
|
||||||
|
"position": [
|
||||||
|
-300.953057070289603,
|
||||||
|
225.21952155696817,
|
||||||
|
139.05608152400566
|
||||||
|
],
|
||||||
|
"scale": 14.151176153817078,
|
||||||
|
"linearVelocity": [
|
||||||
|
1.9861965590557589,
|
||||||
|
-314.387724003424648,
|
||||||
|
-8.922954201633985
|
||||||
|
],
|
||||||
|
"angularVelocity": [
|
||||||
|
0.7016416714654072,
|
||||||
|
-4.8069811132136699,
|
||||||
|
-0.16093262088047533
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "asteroid-10",
|
||||||
|
"position": [
|
||||||
|
300.953057070289603,
|
||||||
|
225.21952155696817,
|
||||||
|
139.05608152400566
|
||||||
|
],
|
||||||
|
"scale": 12,
|
||||||
|
"linearVelocity": [
|
||||||
|
100.9861965590557589,
|
||||||
|
-314.387724003424648,
|
||||||
|
-240.922954201633985
|
||||||
|
],
|
||||||
|
"angularVelocity": [
|
||||||
|
0.7016416714654072,
|
||||||
|
-4.8069811132136699,
|
||||||
|
-0.16093262088047533
|
||||||
|
]
|
||||||
|
},{
|
||||||
|
"id": "asteroid-11",
|
||||||
|
"position": [
|
||||||
|
300.953057070289603,
|
||||||
|
-225.21952155696817,
|
||||||
|
69.05608152400566
|
||||||
|
],
|
||||||
|
"scale": 30,
|
||||||
|
"linearVelocity": [
|
||||||
|
100.9861965590557589,
|
||||||
|
-214.387724003424648,
|
||||||
|
140.922954201633985
|
||||||
|
],
|
||||||
|
"angularVelocity": [
|
||||||
|
0.7016416714654072,
|
||||||
|
-4.8069811132136699,
|
||||||
|
-0.16093262088047533
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
],
|
||||||
|
"difficultyConfig": {
|
||||||
|
"rockCount": 10,
|
||||||
|
"forceMultiplier": 1,
|
||||||
|
"rockSizeMin": 8,
|
||||||
|
"rockSizeMax": 20,
|
||||||
|
"distanceMin": 225,
|
||||||
|
"distanceMax": 300
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.6",
|
"version": "1.0.11",
|
||||||
"levels": [
|
"levels": [
|
||||||
{
|
{
|
||||||
"id": "rookie-training",
|
"id": "rookie-training",
|
||||||
@ -21,21 +21,21 @@
|
|||||||
"defaultLocked": false
|
"defaultLocked": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "rescue-mission",
|
"id": "asteroid-mania",
|
||||||
"name": "Rescue Mission",
|
"name": "Asteroid Mania!!!",
|
||||||
"description": "Rescue operation in moderate asteroid field",
|
"description": "Still low stakes, just more asteroids",
|
||||||
"version": "1.0",
|
"version": "1.1",
|
||||||
"levelPath": "rescue-mission.json",
|
"levelPath": "asteroid-mania.json",
|
||||||
"difficulty": "pilot",
|
"difficulty": "pilot",
|
||||||
"estimatedTime": "5-8 minutes",
|
"estimatedTime": "5-8 minutes",
|
||||||
"missionBrief": [
|
"missionBrief": [
|
||||||
"More asteroids and increased difficulty",
|
"More asteroids and increased difficulty",
|
||||||
"Manage your fuel and ammunition carefully",
|
"Manage your fuel and ammunition carefully",
|
||||||
"Complete the mission and return to base",
|
"Complete the mission and return to base",
|
||||||
"Use your radar to track asteroids",
|
"Watch your hull integrity!",
|
||||||
"Watch your shield strength"
|
"Some of the asteroids are a little more distant"
|
||||||
],
|
],
|
||||||
"unlockRequirements": ["rookie-training"],
|
"unlockRequirements": [],
|
||||||
"tags": ["medium"],
|
"tags": ["medium"],
|
||||||
"defaultLocked": true
|
"defaultLocked": true
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"difficulty": "pilot",
|
"difficulty": "rookie",
|
||||||
"timestamp": "2025-11-11T23:44:24.810Z",
|
"timestamp": "2025-11-11T23:44:24.810Z",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"author": "System",
|
"author": "System",
|
||||||
|
|||||||
@ -4,10 +4,10 @@
|
|||||||
import LevelCard from './LevelCard.svelte';
|
import LevelCard from './LevelCard.svelte';
|
||||||
import ProgressBar from './ProgressBar.svelte';
|
import ProgressBar from './ProgressBar.svelte';
|
||||||
|
|
||||||
// Get default levels in order
|
// Get default levels in order (must match directory.json)
|
||||||
const DEFAULT_LEVEL_ORDER = [
|
const DEFAULT_LEVEL_ORDER = [
|
||||||
'rookie-training',
|
'rookie-training',
|
||||||
'rescue-mission',
|
'asteroid-mania',
|
||||||
'deep-space-patrol',
|
'deep-space-patrol',
|
||||||
'enemy-territory',
|
'enemy-territory',
|
||||||
'the-gauntlet',
|
'the-gauntlet',
|
||||||
|
|||||||
@ -1,96 +0,0 @@
|
|||||||
/**
|
|
||||||
* Simple hash-based client-side router
|
|
||||||
*/
|
|
||||||
export class Router {
|
|
||||||
private routes: Map<string, () => void> = new Map();
|
|
||||||
private currentRoute: string = '';
|
|
||||||
private started: boolean = false;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
// Listen for hash changes
|
|
||||||
window.addEventListener('hashchange', () => this.handleRoute());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the router (call after registering all routes)
|
|
||||||
*/
|
|
||||||
public start(): void {
|
|
||||||
if (!this.started) {
|
|
||||||
this.started = true;
|
|
||||||
this.handleRoute();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a route handler
|
|
||||||
*/
|
|
||||||
public on(path: string, handler: () => void | Promise<void>): void {
|
|
||||||
this.routes.set(path, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to a route programmatically
|
|
||||||
*/
|
|
||||||
public navigate(path: string): void {
|
|
||||||
window.location.hash = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current route path (without #)
|
|
||||||
*/
|
|
||||||
public getCurrentRoute(): string {
|
|
||||||
return this.currentRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle route changes
|
|
||||||
*/
|
|
||||||
private async handleRoute(): Promise<void> {
|
|
||||||
// Get hash without the #
|
|
||||||
let hash = window.location.hash.slice(1) || '/';
|
|
||||||
|
|
||||||
// Normalize route
|
|
||||||
if (!hash.startsWith('/')) {
|
|
||||||
hash = '/' + hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentRoute = hash;
|
|
||||||
|
|
||||||
// Find and execute route handler
|
|
||||||
const handler = this.routes.get(hash);
|
|
||||||
if (handler) {
|
|
||||||
await handler();
|
|
||||||
} else {
|
|
||||||
// Default to root if route not found
|
|
||||||
const defaultHandler = this.routes.get('/');
|
|
||||||
if (defaultHandler) {
|
|
||||||
await defaultHandler();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global router instance
|
|
||||||
export const router = new Router();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to show/hide views
|
|
||||||
*/
|
|
||||||
export function showView(viewId: string): void {
|
|
||||||
console.log('[Router] showView() called with viewId:', viewId);
|
|
||||||
|
|
||||||
// Hide all views
|
|
||||||
const views = document.querySelectorAll('[data-view]');
|
|
||||||
console.log('[Router] Found views:', views.length);
|
|
||||||
views.forEach(view => {
|
|
||||||
(view as HTMLElement).style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show requested view
|
|
||||||
const targetView = document.querySelector(`[data-view="${viewId}"]`);
|
|
||||||
console.log('[Router] Target view found:', !!targetView);
|
|
||||||
if (targetView) {
|
|
||||||
(targetView as HTMLElement).style.display = 'block';
|
|
||||||
console.log('[Router] View display set to block');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -123,11 +123,15 @@ export class RockFactory {
|
|||||||
// PhysicsAggregate will automatically compute sphere size from mesh bounding info
|
// PhysicsAggregate will automatically compute sphere size from mesh bounding info
|
||||||
// The mesh scaling is already applied, so Babylon will create correctly sized physics shape
|
// The mesh scaling is already applied, so Babylon will create correctly sized physics shape
|
||||||
const agg = new PhysicsAggregate(rock, PhysicsShapeType.SPHERE, {
|
const agg = new PhysicsAggregate(rock, PhysicsShapeType.SPHERE, {
|
||||||
mass: 10000,
|
mass: 200,
|
||||||
restitution: .5
|
friction: 0,
|
||||||
|
restitution: .8
|
||||||
// Don't pass radius - let Babylon compute from scaled mesh bounds
|
// Don't pass radius - let Babylon compute from scaled mesh bounds
|
||||||
}, DefaultScene.MainScene);
|
}, DefaultScene.MainScene);
|
||||||
const body = agg.body;
|
const body = agg.body;
|
||||||
|
body.setAngularDamping(0);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Only apply orbit constraint if enabled for this level and orbit center exists
|
// Only apply orbit constraint if enabled for this level and orbit center exists
|
||||||
if (useOrbitConstraint && this._orbitCenter) {
|
if (useOrbitConstraint && this._orbitCenter) {
|
||||||
|
|||||||
@ -36,11 +36,10 @@ export interface LevelRegistryEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CUSTOM_LEVELS_KEY = 'space-game-custom-levels';
|
const CUSTOM_LEVELS_KEY = 'space-game-custom-levels';
|
||||||
const CACHE_NAME = 'space-game-levels-v1';
|
|
||||||
const CACHED_VERSION_KEY = 'space-game-levels-cached-version';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton registry for managing both default and custom levels
|
* Singleton registry for managing both default and custom levels
|
||||||
|
* Always fetches fresh from network - no caching
|
||||||
*/
|
*/
|
||||||
export class LevelRegistry {
|
export class LevelRegistry {
|
||||||
private static instance: LevelRegistry | null = null;
|
private static instance: LevelRegistry | null = null;
|
||||||
@ -63,121 +62,49 @@ export class LevelRegistry {
|
|||||||
* Initialize the registry by loading directory and levels
|
* Initialize the registry by loading directory and levels
|
||||||
*/
|
*/
|
||||||
public async initialize(): Promise<void> {
|
public async initialize(): Promise<void> {
|
||||||
console.log('[LevelRegistry] initialize() called, initialized =', this.initialized);
|
|
||||||
|
|
||||||
if (this.initialized) {
|
if (this.initialized) {
|
||||||
console.log('[LevelRegistry] Already initialized, skipping');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[LevelRegistry] Loading directory manifest...');
|
|
||||||
// Load directory manifest
|
|
||||||
await this.loadDirectory();
|
await this.loadDirectory();
|
||||||
console.log('[LevelRegistry] Directory loaded, entries:', this.directoryManifest?.levels.length);
|
|
||||||
|
|
||||||
console.log('[LevelRegistry] Loading custom levels from localStorage...');
|
|
||||||
// Load custom levels from localStorage
|
|
||||||
this.loadCustomLevels();
|
this.loadCustomLevels();
|
||||||
console.log('[LevelRegistry] Custom levels loaded:', this.customLevels.size);
|
|
||||||
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
console.log('[LevelRegistry] Initialization complete!');
|
console.log('[LevelRegistry] Initialized with', this.defaultLevels.size, 'default levels');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LevelRegistry] Failed to initialize level registry:', error);
|
console.error('[LevelRegistry] Failed to initialize:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the directory.json manifest
|
* Check if running in development mode (for cache-busting HTTP requests)
|
||||||
|
*/
|
||||||
|
private isDevMode(): boolean {
|
||||||
|
return window.location.hostname === 'localhost' ||
|
||||||
|
window.location.hostname.includes('dev.') ||
|
||||||
|
window.location.port !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the directory.json manifest (always fresh from network)
|
||||||
*/
|
*/
|
||||||
private async loadDirectory(): Promise<void> {
|
private async loadDirectory(): Promise<void> {
|
||||||
console.log('[LevelRegistry] ======================================');
|
|
||||||
console.log('[LevelRegistry] loadDirectory() ENTERED at', Date.now());
|
|
||||||
console.log('[LevelRegistry] ======================================');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[LevelRegistry] Attempting to fetch /levels/directory.json');
|
// Add cache-busting in dev mode to avoid browser HTTP cache
|
||||||
console.log('[LevelRegistry] window.location.origin:', window.location.origin);
|
const cacheBuster = this.isDevMode() ? `?v=${Date.now()}` : '';
|
||||||
console.log('[LevelRegistry] Full URL will be:', window.location.origin + '/levels/directory.json');
|
|
||||||
|
|
||||||
// First, fetch from network to get the latest version
|
|
||||||
console.log('[LevelRegistry] About to call fetch() - Timestamp:', Date.now());
|
|
||||||
console.log('[LevelRegistry] Fetching from network to check version...');
|
|
||||||
|
|
||||||
// Add cache-busting for development or when debugging
|
|
||||||
const isDev = window.location.hostname === 'localhost' ||
|
|
||||||
window.location.hostname.includes('dev.') ||
|
|
||||||
window.location.port !== '';
|
|
||||||
const cacheBuster = isDev ? `?v=${Date.now()}` : '';
|
|
||||||
console.log('[LevelRegistry] Cache busting:', isDev ? 'ENABLED (dev mode)' : 'DISABLED (production)');
|
|
||||||
|
|
||||||
const fetchStartTime = Date.now();
|
|
||||||
const response = await fetch(`/levels/directory.json${cacheBuster}`);
|
const response = await fetch(`/levels/directory.json${cacheBuster}`);
|
||||||
const fetchEndTime = Date.now();
|
|
||||||
|
|
||||||
console.log('[LevelRegistry] fetch() returned! Time taken:', fetchEndTime - fetchStartTime, 'ms');
|
|
||||||
console.log('[LevelRegistry] Fetch response status:', response.status, response.ok);
|
|
||||||
console.log('[LevelRegistry] Fetch response type:', response.type);
|
|
||||||
console.log('[LevelRegistry] Fetch response headers:', {
|
|
||||||
contentType: response.headers.get('content-type'),
|
|
||||||
contentLength: response.headers.get('content-length')
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// If network fails, try to use cached version as fallback
|
|
||||||
console.warn('[LevelRegistry] Network fetch failed, trying cache...');
|
|
||||||
const cached = await this.getCachedResource('/levels/directory.json');
|
|
||||||
if (cached) {
|
|
||||||
console.log('[LevelRegistry] Using cached directory as fallback');
|
|
||||||
this.directoryManifest = cached;
|
|
||||||
this.populateDefaultLevelEntries();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to fetch directory: ${response.status}`);
|
throw new Error(`Failed to fetch directory: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[LevelRegistry] About to parse response.json()');
|
this.directoryManifest = await response.json();
|
||||||
const parseStartTime = Date.now();
|
console.log('[LevelRegistry] Loaded directory with', this.directoryManifest?.levels?.length || 0, 'levels');
|
||||||
const networkManifest = await response.json();
|
|
||||||
const parseEndTime = Date.now();
|
|
||||||
console.log('[LevelRegistry] JSON parsed successfully! Time taken:', parseEndTime - parseStartTime, 'ms');
|
|
||||||
console.log('[LevelRegistry] Directory JSON parsed:', networkManifest);
|
|
||||||
console.log('[LevelRegistry] Number of levels in manifest:', networkManifest?.levels?.length || 0);
|
|
||||||
|
|
||||||
// Check if version changed
|
|
||||||
const cachedVersion = localStorage.getItem(CACHED_VERSION_KEY);
|
|
||||||
const currentVersion = networkManifest.version;
|
|
||||||
|
|
||||||
if (cachedVersion && cachedVersion !== currentVersion) {
|
|
||||||
console.log('[LevelRegistry] Version changed from', cachedVersion, 'to', currentVersion, '- invalidating cache');
|
|
||||||
await this.invalidateCache();
|
|
||||||
} else {
|
|
||||||
console.log('[LevelRegistry] Version unchanged or first load:', currentVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cached version
|
|
||||||
localStorage.setItem(CACHED_VERSION_KEY, currentVersion);
|
|
||||||
|
|
||||||
// Store the manifest
|
|
||||||
this.directoryManifest = networkManifest;
|
|
||||||
|
|
||||||
// Cache the directory
|
|
||||||
await this.cacheResource('/levels/directory.json', this.directoryManifest);
|
|
||||||
|
|
||||||
console.log('[LevelRegistry] About to populate default level entries');
|
|
||||||
this.populateDefaultLevelEntries();
|
this.populateDefaultLevelEntries();
|
||||||
console.log('[LevelRegistry] Default level entries populated successfully');
|
|
||||||
console.log('[LevelRegistry] ======================================');
|
|
||||||
console.log('[LevelRegistry] loadDirectory() COMPLETED at', Date.now());
|
|
||||||
console.log('[LevelRegistry] ======================================');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LevelRegistry] !!!!! EXCEPTION in loadDirectory() !!!!!');
|
|
||||||
console.error('[LevelRegistry] Failed to load directory:', error);
|
console.error('[LevelRegistry] Failed to load directory:', error);
|
||||||
console.error('[LevelRegistry] Error type:', error?.constructor?.name);
|
|
||||||
console.error('[LevelRegistry] Error message:', error?.message);
|
|
||||||
console.error('[LevelRegistry] Error stack:', error?.stack);
|
|
||||||
throw new Error('Unable to load level directory. Please check your connection.');
|
throw new Error('Unable to load level directory. Please check your connection.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,27 +114,12 @@ export class LevelRegistry {
|
|||||||
*/
|
*/
|
||||||
private populateDefaultLevelEntries(): void {
|
private populateDefaultLevelEntries(): void {
|
||||||
if (!this.directoryManifest) {
|
if (!this.directoryManifest) {
|
||||||
console.error('[LevelRegistry] ❌ Cannot populate - directoryManifest is null');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[LevelRegistry] ======================================');
|
|
||||||
console.log('[LevelRegistry] Populating default level entries...');
|
|
||||||
console.log('[LevelRegistry] Directory manifest levels:', this.directoryManifest.levels.length);
|
|
||||||
|
|
||||||
this.defaultLevels.clear();
|
this.defaultLevels.clear();
|
||||||
|
|
||||||
for (const entry of this.directoryManifest.levels) {
|
for (const entry of this.directoryManifest.levels) {
|
||||||
console.log(`[LevelRegistry] Storing level: ${entry.id}`, {
|
|
||||||
name: entry.name,
|
|
||||||
levelPath: entry.levelPath,
|
|
||||||
hasMissionBrief: !!entry.missionBrief,
|
|
||||||
missionBriefItems: entry.missionBrief?.length || 0,
|
|
||||||
hasLevelPath: !!entry.levelPath,
|
|
||||||
estimatedTime: entry.estimatedTime,
|
|
||||||
difficulty: entry.difficulty
|
|
||||||
});
|
|
||||||
|
|
||||||
this.defaultLevels.set(entry.id, {
|
this.defaultLevels.set(entry.id, {
|
||||||
directoryEntry: entry,
|
directoryEntry: entry,
|
||||||
config: null, // Lazy load
|
config: null, // Lazy load
|
||||||
@ -215,9 +127,7 @@ export class LevelRegistry {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[LevelRegistry] Populated entries. Total count:', this.defaultLevels.size);
|
|
||||||
console.log('[LevelRegistry] Level IDs:', Array.from(this.defaultLevels.keys()));
|
console.log('[LevelRegistry] Level IDs:', Array.from(this.defaultLevels.keys()));
|
||||||
console.log('[LevelRegistry] ======================================');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -241,7 +151,7 @@ export class LevelRegistry {
|
|||||||
name: config.metadata?.description || id,
|
name: config.metadata?.description || id,
|
||||||
description: config.metadata?.description || '',
|
description: config.metadata?.description || '',
|
||||||
version: config.version || '1.0',
|
version: config.version || '1.0',
|
||||||
levelPath: '', // Not applicable for custom
|
levelPath: '',
|
||||||
difficulty: config.difficulty,
|
difficulty: config.difficulty,
|
||||||
missionBrief: [],
|
missionBrief: [],
|
||||||
defaultLocked: false
|
defaultLocked: false
|
||||||
@ -275,91 +185,30 @@ export class LevelRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a default level's config from JSON
|
* Load a default level's config from JSON (always fresh from network)
|
||||||
*/
|
*/
|
||||||
private async loadDefaultLevel(levelId: string): Promise<void> {
|
private async loadDefaultLevel(levelId: string): Promise<void> {
|
||||||
console.log('[LevelRegistry] ======================================');
|
|
||||||
console.log('[LevelRegistry] loadDefaultLevel() called for:', levelId);
|
|
||||||
console.log('[LevelRegistry] Timestamp:', Date.now());
|
|
||||||
console.log('[LevelRegistry] ======================================');
|
|
||||||
|
|
||||||
const entry = this.defaultLevels.get(levelId);
|
const entry = this.defaultLevels.get(levelId);
|
||||||
if (!entry || entry.config) {
|
if (!entry || entry.config) {
|
||||||
console.log('[LevelRegistry] Early return - entry:', !!entry, ', config loaded:', !!entry?.config);
|
return;
|
||||||
return; // Already loaded or doesn't exist
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const levelPath = `/levels/${entry.directoryEntry.levelPath}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const levelPath = `/levels/${entry.directoryEntry.levelPath}`;
|
// Add cache-busting in dev mode
|
||||||
console.log('[LevelRegistry] Constructed levelPath:', levelPath);
|
const cacheBuster = this.isDevMode() ? `?v=${Date.now()}` : '';
|
||||||
console.log('[LevelRegistry] Full URL will be:', window.location.origin + levelPath);
|
|
||||||
|
|
||||||
// Check if cache busting is enabled (dev mode)
|
|
||||||
const isDev = window.location.hostname === 'localhost' ||
|
|
||||||
window.location.hostname.includes('dev.') ||
|
|
||||||
window.location.port !== '';
|
|
||||||
|
|
||||||
// In dev mode, skip cache and always fetch fresh
|
|
||||||
let cached = null;
|
|
||||||
if (!isDev) {
|
|
||||||
console.log('[LevelRegistry] Checking cache for:', levelPath);
|
|
||||||
cached = await this.getCachedResource(levelPath);
|
|
||||||
} else {
|
|
||||||
console.log('[LevelRegistry] Skipping cache check (dev mode)');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cached) {
|
|
||||||
console.log('[LevelRegistry] Found in cache! Using cached config');
|
|
||||||
entry.config = cached;
|
|
||||||
entry.loadedAt = new Date();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('[LevelRegistry] Not in cache, fetching from network');
|
|
||||||
|
|
||||||
// Fetch from network with cache-busting in dev mode
|
|
||||||
const cacheBuster = isDev ? `?v=${Date.now()}` : '';
|
|
||||||
console.log('[LevelRegistry] About to fetch level JSON - Timestamp:', Date.now());
|
|
||||||
console.log('[LevelRegistry] Cache busting:', isDev ? 'ENABLED' : 'DISABLED');
|
|
||||||
const fetchStartTime = Date.now();
|
|
||||||
const response = await fetch(`${levelPath}${cacheBuster}`);
|
const response = await fetch(`${levelPath}${cacheBuster}`);
|
||||||
const fetchEndTime = Date.now();
|
|
||||||
|
|
||||||
console.log('[LevelRegistry] Level fetch() returned! Time taken:', fetchEndTime - fetchStartTime, 'ms');
|
|
||||||
console.log('[LevelRegistry] Response status:', response.status, response.ok);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error('[LevelRegistry] Fetch failed with status:', response.status);
|
|
||||||
throw new Error(`Failed to fetch level: ${response.status}`);
|
throw new Error(`Failed to fetch level: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[LevelRegistry] Parsing level JSON...');
|
entry.config = await response.json();
|
||||||
const parseStartTime = Date.now();
|
|
||||||
const config: LevelConfig = await response.json();
|
|
||||||
const parseEndTime = Date.now();
|
|
||||||
console.log('[LevelRegistry] Level JSON parsed! Time taken:', parseEndTime - parseStartTime, 'ms');
|
|
||||||
console.log('[LevelRegistry] Level config loaded:', {
|
|
||||||
version: config.version,
|
|
||||||
difficulty: config.difficulty,
|
|
||||||
asteroidCount: config.asteroids?.length || 0
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cache the level
|
|
||||||
console.log('[LevelRegistry] Caching level config...');
|
|
||||||
await this.cacheResource(levelPath, config);
|
|
||||||
console.log('[LevelRegistry] Level cached successfully');
|
|
||||||
|
|
||||||
entry.config = config;
|
|
||||||
entry.loadedAt = new Date();
|
entry.loadedAt = new Date();
|
||||||
|
console.log('[LevelRegistry] Loaded level:', levelId);
|
||||||
console.log('[LevelRegistry] ======================================');
|
|
||||||
console.log('[LevelRegistry] loadDefaultLevel() COMPLETED for:', levelId);
|
|
||||||
console.log('[LevelRegistry] ======================================');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LevelRegistry] !!!!! EXCEPTION in loadDefaultLevel() !!!!!');
|
console.error(`[LevelRegistry] Failed to load level ${levelId}:`, error);
|
||||||
console.error(`[LevelRegistry] Failed to load default level ${levelId}:`, error);
|
|
||||||
console.error('[LevelRegistry] Error type:', error?.constructor?.name);
|
|
||||||
console.error('[LevelRegistry] Error message:', error?.message);
|
|
||||||
console.error('[LevelRegistry] Error stack:', error?.stack);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -370,12 +219,10 @@ export class LevelRegistry {
|
|||||||
public getAllLevels(): Map<string, LevelRegistryEntry> {
|
public getAllLevels(): Map<string, LevelRegistryEntry> {
|
||||||
const all = new Map<string, LevelRegistryEntry>();
|
const all = new Map<string, LevelRegistryEntry>();
|
||||||
|
|
||||||
// Add defaults
|
|
||||||
for (const [id, entry] of this.defaultLevels) {
|
for (const [id, entry] of this.defaultLevels) {
|
||||||
all.set(id, entry);
|
all.set(id, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add customs
|
|
||||||
for (const [id, entry] of this.customLevels) {
|
for (const [id, entry] of this.customLevels) {
|
||||||
all.set(id, entry);
|
all.set(id, entry);
|
||||||
}
|
}
|
||||||
@ -401,7 +248,6 @@ export class LevelRegistry {
|
|||||||
* Save a custom level
|
* Save a custom level
|
||||||
*/
|
*/
|
||||||
public saveCustomLevel(levelId: string, config: LevelConfig): void {
|
public saveCustomLevel(levelId: string, config: LevelConfig): void {
|
||||||
// Ensure metadata exists
|
|
||||||
if (!config.metadata) {
|
if (!config.metadata) {
|
||||||
config.metadata = {
|
config.metadata = {
|
||||||
author: 'Player',
|
author: 'Player',
|
||||||
@ -409,12 +255,10 @@ export class LevelRegistry {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove 'default' type if present
|
|
||||||
if (config.metadata.type === 'default') {
|
if (config.metadata.type === 'default') {
|
||||||
delete config.metadata.type;
|
delete config.metadata.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add/update in memory
|
|
||||||
this.customLevels.set(levelId, {
|
this.customLevels.set(levelId, {
|
||||||
directoryEntry: {
|
directoryEntry: {
|
||||||
id: levelId,
|
id: levelId,
|
||||||
@ -431,7 +275,6 @@ export class LevelRegistry {
|
|||||||
loadedAt: new Date()
|
loadedAt: new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Persist to localStorage
|
|
||||||
this.saveCustomLevelsToStorage();
|
this.saveCustomLevelsToStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -455,10 +298,8 @@ export class LevelRegistry {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deep clone the config
|
|
||||||
const clonedConfig: LevelConfig = JSON.parse(JSON.stringify(config));
|
const clonedConfig: LevelConfig = JSON.parse(JSON.stringify(config));
|
||||||
|
|
||||||
// Update metadata
|
|
||||||
clonedConfig.metadata = {
|
clonedConfig.metadata = {
|
||||||
...clonedConfig.metadata,
|
...clonedConfig.metadata,
|
||||||
type: undefined,
|
type: undefined,
|
||||||
@ -487,72 +328,14 @@ export class LevelRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a resource from cache
|
* Force refresh all default levels from network
|
||||||
*/
|
*/
|
||||||
private async getCachedResource(path: string): Promise<any | null> {
|
public async refreshDefaultLevels(): Promise<void> {
|
||||||
if (!('caches' in window)) {
|
// Clear in-memory configs
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cache = await caches.open(CACHE_NAME);
|
|
||||||
const response = await cache.match(path);
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Cache read failed:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache a resource
|
|
||||||
*/
|
|
||||||
private async cacheResource(path: string, data: any): Promise<void> {
|
|
||||||
if (!('caches' in window)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cache = await caches.open(CACHE_NAME);
|
|
||||||
const response = new Response(JSON.stringify(data), {
|
|
||||||
headers: {'Content-Type': 'application/json'}
|
|
||||||
});
|
|
||||||
await cache.put(path, response);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Cache write failed:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidate the entire cache (called when version changes)
|
|
||||||
*/
|
|
||||||
private async invalidateCache(): Promise<void> {
|
|
||||||
console.log('[LevelRegistry] Invalidating cache...');
|
|
||||||
if ('caches' in window) {
|
|
||||||
await caches.delete(CACHE_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear loaded configs
|
|
||||||
for (const entry of this.defaultLevels.values()) {
|
for (const entry of this.defaultLevels.values()) {
|
||||||
entry.config = null;
|
entry.config = null;
|
||||||
entry.loadedAt = undefined;
|
entry.loadedAt = undefined;
|
||||||
}
|
}
|
||||||
console.log('[LevelRegistry] Cache invalidated');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Force refresh all default levels from network
|
|
||||||
*/
|
|
||||||
public async refreshDefaultLevels(): Promise<void> {
|
|
||||||
// Clear cache
|
|
||||||
await this.invalidateCache();
|
|
||||||
|
|
||||||
// Clear cached version to force re-check
|
|
||||||
localStorage.removeItem(CACHED_VERSION_KEY);
|
|
||||||
|
|
||||||
// Reload directory
|
// Reload directory
|
||||||
await this.loadDirectory();
|
await this.loadDirectory();
|
||||||
@ -608,36 +391,17 @@ export class LevelRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all caches and force reload from network
|
* Reset registry state (for testing or force reload)
|
||||||
* Useful for development or when data needs to be refreshed
|
|
||||||
*/
|
*/
|
||||||
public async clearAllCaches(): Promise<void> {
|
public reset(): void {
|
||||||
console.log('[LevelRegistry] Clearing all caches...');
|
|
||||||
|
|
||||||
// Clear Cache API
|
|
||||||
if ('caches' in window) {
|
|
||||||
const cacheKeys = await caches.keys();
|
|
||||||
for (const key of cacheKeys) {
|
|
||||||
await caches.delete(key);
|
|
||||||
console.log('[LevelRegistry] Deleted cache:', key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear localStorage cache version
|
|
||||||
localStorage.removeItem(CACHED_VERSION_KEY);
|
|
||||||
console.log('[LevelRegistry] Cleared localStorage cache version');
|
|
||||||
|
|
||||||
// Clear loaded configs
|
|
||||||
for (const entry of this.defaultLevels.values()) {
|
for (const entry of this.defaultLevels.values()) {
|
||||||
entry.config = null;
|
entry.config = null;
|
||||||
entry.loadedAt = undefined;
|
entry.loadedAt = undefined;
|
||||||
}
|
}
|
||||||
console.log('[LevelRegistry] Cleared loaded configs');
|
|
||||||
|
|
||||||
// Reset initialization flag to force reload
|
|
||||||
this.initialized = false;
|
this.initialized = false;
|
||||||
this.directoryManifest = null;
|
this.directoryManifest = null;
|
||||||
|
|
||||||
console.log('[LevelRegistry] All caches cleared. Call initialize() to reload.');
|
console.log('[LevelRegistry] Reset complete. Call initialize() to reload.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,416 +0,0 @@
|
|||||||
import {LevelConfig} from "../config/levelConfig";
|
|
||||||
import {ProgressionManager} from "../../game/progression";
|
|
||||||
import {GameConfig} from "../../core/gameConfig";
|
|
||||||
import {AuthService} from "../../services/authService";
|
|
||||||
import debugLog from '../../core/debug';
|
|
||||||
import {LevelRegistry} from "../storage/levelRegistry";
|
|
||||||
import {LevelVersionManager} from "../versioning/levelVersionManager";
|
|
||||||
import {LevelStatsManager} from "../stats/levelStats";
|
|
||||||
|
|
||||||
const SELECTED_LEVEL_KEY = 'space-game-selected-level';
|
|
||||||
|
|
||||||
// Default level IDs in display order (matches directory.json)
|
|
||||||
const DEFAULT_LEVEL_ORDER = [
|
|
||||||
'rookie-training',
|
|
||||||
'rescue-mission',
|
|
||||||
'deep-space-patrol',
|
|
||||||
'enemy-territory',
|
|
||||||
'the-gauntlet',
|
|
||||||
'final-challenge'
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Populate the level selection screen with levels from registry
|
|
||||||
* Shows all 6 default levels in a 3x2 carousel with locked/unlocked states
|
|
||||||
*/
|
|
||||||
export async function populateLevelSelector(): Promise<boolean> {
|
|
||||||
console.log('[LevelSelector] populateLevelSelector() called');
|
|
||||||
const container = document.getElementById('levelCardsContainer');
|
|
||||||
if (!container) {
|
|
||||||
console.warn('[LevelSelector] Level cards container not found');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
console.log('[LevelSelector] Container found:', container);
|
|
||||||
|
|
||||||
const registry = LevelRegistry.getInstance();
|
|
||||||
const versionManager = LevelVersionManager.getInstance();
|
|
||||||
const statsManager = LevelStatsManager.getInstance();
|
|
||||||
|
|
||||||
// Initialize registry
|
|
||||||
try {
|
|
||||||
console.log('[LevelSelector] Initializing registry...');
|
|
||||||
await registry.initialize();
|
|
||||||
console.log('[LevelSelector] Registry initialized');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[LevelSelector] Registry initialization error:', error);
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="no-levels-message">
|
|
||||||
<h2>Failed to Load Levels</h2>
|
|
||||||
<p>Could not load level directory. Check your connection and try again.</p>
|
|
||||||
<button onclick="location.reload()" class="btn-primary">Reload</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gameConfig = GameConfig.getInstance();
|
|
||||||
const progressionEnabled = gameConfig.progressionEnabled;
|
|
||||||
const progression = ProgressionManager.getInstance();
|
|
||||||
|
|
||||||
// Update version manager with directory
|
|
||||||
const directory = registry.getDirectory();
|
|
||||||
if (directory) {
|
|
||||||
versionManager.updateManifestVersions(directory);
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultLevels = registry.getDefaultLevels();
|
|
||||||
const customLevels = registry.getCustomLevels();
|
|
||||||
|
|
||||||
console.log('[LevelSelector] Default levels:', defaultLevels.size);
|
|
||||||
console.log('[LevelSelector] Custom levels:', customLevels.size);
|
|
||||||
console.log('[LevelSelector] Default level IDs:', Array.from(defaultLevels.keys()));
|
|
||||||
|
|
||||||
if (defaultLevels.size === 0 && customLevels.size === 0) {
|
|
||||||
console.warn('[LevelSelector] No levels found!');
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="no-levels-message">
|
|
||||||
<h2>No Levels Found</h2>
|
|
||||||
<p>No levels available. Please check your installation.</p>
|
|
||||||
<a href="#/editor" class="btn-primary">Create Custom Level</a>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
const authService = AuthService.getInstance();
|
|
||||||
const isAuthenticated = await authService.isAuthenticated();
|
|
||||||
const isTutorial = (levelId: string) => levelId === DEFAULT_LEVEL_ORDER[0];
|
|
||||||
|
|
||||||
debugLog('[LevelSelector] Authenticated:', isAuthenticated);
|
|
||||||
debugLog('[LevelSelector] Progression enabled:', progressionEnabled);
|
|
||||||
debugLog('[LevelSelector] Default levels count:', defaultLevels.size);
|
|
||||||
|
|
||||||
// Show all default levels in order (3x2 grid)
|
|
||||||
if (defaultLevels.size > 0) {
|
|
||||||
for (const levelId of DEFAULT_LEVEL_ORDER) {
|
|
||||||
const entry = defaultLevels.get(levelId);
|
|
||||||
|
|
||||||
if (!entry) {
|
|
||||||
// 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">Missing Level</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 dirEntry = entry.directoryEntry;
|
|
||||||
const levelName = dirEntry.name;
|
|
||||||
const description = dirEntry.description;
|
|
||||||
const estimatedTime = dirEntry.estimatedTime || '';
|
|
||||||
const difficulty = dirEntry.difficulty || 'unknown';
|
|
||||||
|
|
||||||
// Check for version updates
|
|
||||||
const hasUpdate = versionManager.hasUpdate(levelId);
|
|
||||||
|
|
||||||
// Get stats
|
|
||||||
const stats = statsManager.getStats(levelId);
|
|
||||||
const completionRate = stats?.completionRate || 0;
|
|
||||||
const bestTime = stats?.bestTimeSeconds;
|
|
||||||
|
|
||||||
// Check progression
|
|
||||||
const isCompleted = progressionEnabled && progression.isLevelComplete(levelName);
|
|
||||||
|
|
||||||
// Check if level is unlocked
|
|
||||||
let isUnlocked = false;
|
|
||||||
const isTut = isTutorial(levelId);
|
|
||||||
|
|
||||||
if (isTut) {
|
|
||||||
isUnlocked = true; // Tutorial always unlocked
|
|
||||||
} else if (!isAuthenticated) {
|
|
||||||
isUnlocked = false; // Non-tutorial levels require authentication
|
|
||||||
} else {
|
|
||||||
isUnlocked = !progressionEnabled || progression.isLevelUnlocked(levelName);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCurrentNext = progressionEnabled && progression.getNextLevel() === levelName;
|
|
||||||
|
|
||||||
// Determine card state
|
|
||||||
let cardClasses = 'level-card';
|
|
||||||
let statusIcons = '';
|
|
||||||
let buttonText = 'Play Level';
|
|
||||||
let buttonDisabled = '';
|
|
||||||
let lockReason = '';
|
|
||||||
let metaTags = '';
|
|
||||||
|
|
||||||
// Version update badge
|
|
||||||
if (hasUpdate) {
|
|
||||||
statusIcons += '<div class="level-card-badge level-card-badge-update">UPDATED</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCompleted) {
|
|
||||||
cardClasses += ' level-card-completed';
|
|
||||||
statusIcons += '<div class="level-card-status level-card-status-complete">✓</div>';
|
|
||||||
buttonText = 'Replay';
|
|
||||||
} else if (isCurrentNext && isUnlocked) {
|
|
||||||
cardClasses += ' level-card-current';
|
|
||||||
statusIcons += '<div class="level-card-badge">START HERE</div>';
|
|
||||||
} else if (!isUnlocked) {
|
|
||||||
cardClasses += ' level-card-locked';
|
|
||||||
statusIcons += '<div class="level-card-status level-card-status-locked">🔒</div>';
|
|
||||||
|
|
||||||
// Determine why it's locked
|
|
||||||
if (!isAuthenticated && !isTutorial(levelId)) {
|
|
||||||
buttonText = 'Sign In Required';
|
|
||||||
lockReason = '<div class="level-lock-reason">Sign in to unlock</div>';
|
|
||||||
} else if (progressionEnabled) {
|
|
||||||
const levelIndex = DEFAULT_LEVEL_ORDER.indexOf(levelId);
|
|
||||||
if (levelIndex > 0) {
|
|
||||||
const prevId = DEFAULT_LEVEL_ORDER[levelIndex - 1];
|
|
||||||
const prevEntry = defaultLevels.get(prevId);
|
|
||||||
const prevName = prevEntry?.directoryEntry.name || 'previous level';
|
|
||||||
lockReason = `<div class="level-lock-reason">Complete "${prevName}" to unlock</div>`;
|
|
||||||
}
|
|
||||||
buttonText = 'Locked';
|
|
||||||
} else {
|
|
||||||
buttonText = 'Locked';
|
|
||||||
}
|
|
||||||
buttonDisabled = ' disabled';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show stats if available
|
|
||||||
if (stats && stats.totalAttempts > 0) {
|
|
||||||
metaTags = '<div class="level-stats">';
|
|
||||||
if (bestTime) {
|
|
||||||
metaTags += `<span class="stat-badge">⏱️ ${LevelStatsManager.formatTime(bestTime)}</span>`;
|
|
||||||
}
|
|
||||||
if (stats.totalCompletions > 0) {
|
|
||||||
metaTags += `<span class="stat-badge">✓ ${stats.totalCompletions}</span>`;
|
|
||||||
}
|
|
||||||
metaTags += `<span class="stat-badge">${LevelStatsManager.formatCompletionRate(completionRate)}</span>`;
|
|
||||||
metaTags += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += `
|
|
||||||
<div class="${cardClasses}">
|
|
||||||
<div class="level-card-header">
|
|
||||||
<h2 class="level-card-title">${levelName}</h2>
|
|
||||||
<div class="level-card-badges">${statusIcons}</div>
|
|
||||||
</div>
|
|
||||||
<div class="level-meta">
|
|
||||||
Difficulty: ${difficulty}${estimatedTime ? ` • ${estimatedTime}` : ''}
|
|
||||||
</div>
|
|
||||||
<p class="level-card-description">${description}</p>
|
|
||||||
${metaTags}
|
|
||||||
${lockReason}
|
|
||||||
<div class="level-card-actions">
|
|
||||||
<button class="level-button" data-level-id="${levelId}"${buttonDisabled}>${buttonText}</button>
|
|
||||||
${entry.isDefault && isUnlocked ? `<button class="level-button-secondary" data-copy-level="${levelId}" title="Copy to custom levels">📋 Copy</button>` : ''}
|
|
||||||
</div>
|
|
||||||
</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 [levelId, entry] of customLevels.entries()) {
|
|
||||||
const config = entry.config;
|
|
||||||
if (!config) continue;
|
|
||||||
|
|
||||||
const description = config.metadata?.description || `${config.asteroids.length} asteroids`;
|
|
||||||
const author = config.metadata?.author ? ` by ${config.metadata.author}` : '';
|
|
||||||
const difficulty = config.difficulty || 'custom';
|
|
||||||
|
|
||||||
// Get stats
|
|
||||||
const stats = statsManager.getStats(levelId);
|
|
||||||
const bestTime = stats?.bestTimeSeconds;
|
|
||||||
let metaTags = '';
|
|
||||||
|
|
||||||
if (stats && stats.totalAttempts > 0) {
|
|
||||||
metaTags = '<div class="level-stats">';
|
|
||||||
if (bestTime) {
|
|
||||||
metaTags += `<span class="stat-badge">⏱️ ${LevelStatsManager.formatTime(bestTime)}</span>`;
|
|
||||||
}
|
|
||||||
if (stats.totalCompletions > 0) {
|
|
||||||
metaTags += `<span class="stat-badge">✓ ${stats.totalCompletions}</span>`;
|
|
||||||
}
|
|
||||||
metaTags += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += `
|
|
||||||
<div class="level-card">
|
|
||||||
<div class="level-card-header">
|
|
||||||
<h2 class="level-card-title">${levelId}</h2>
|
|
||||||
<div class="level-card-badge level-card-badge-custom">CUSTOM</div>
|
|
||||||
</div>
|
|
||||||
<div class="level-meta">
|
|
||||||
${difficulty}${author}
|
|
||||||
</div>
|
|
||||||
<p class="level-card-description">${description}</p>
|
|
||||||
${metaTags}
|
|
||||||
<div class="level-card-actions">
|
|
||||||
<button class="level-button" data-level-id="${levelId}">Play Level</button>
|
|
||||||
<button class="level-button-secondary" data-delete-level="${levelId}" title="Delete level">🗑️</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[LevelSelector] Setting container innerHTML, html length:', html.length);
|
|
||||||
container.innerHTML = html;
|
|
||||||
console.log('[LevelSelector] Container innerHTML set, now attaching event listeners');
|
|
||||||
|
|
||||||
// Attach event listeners to all level buttons
|
|
||||||
const playButtons = container.querySelectorAll('.level-button:not([disabled])');
|
|
||||||
playButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', (e) => {
|
|
||||||
const target = e.target as HTMLButtonElement;
|
|
||||||
const levelId = target.getAttribute('data-level-id');
|
|
||||||
if (levelId) {
|
|
||||||
selectLevel(levelId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Attach copy button listeners
|
|
||||||
const copyButtons = container.querySelectorAll('[data-copy-level]');
|
|
||||||
copyButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', async (e) => {
|
|
||||||
const target = e.target as HTMLButtonElement;
|
|
||||||
const levelId = target.getAttribute('data-copy-level');
|
|
||||||
if (levelId) {
|
|
||||||
await copyLevelToCustom(levelId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Attach delete button listeners
|
|
||||||
const deleteButtons = container.querySelectorAll('[data-delete-level]');
|
|
||||||
deleteButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', (e) => {
|
|
||||||
const target = e.target as HTMLButtonElement;
|
|
||||||
const levelId = target.getAttribute('data-delete-level');
|
|
||||||
if (levelId) {
|
|
||||||
deleteCustomLevel(levelId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[LevelSelector] Event listeners attached, returning true');
|
|
||||||
|
|
||||||
// Make the level selector visible by adding 'ready' class
|
|
||||||
const levelSelectDiv = document.getElementById('levelSelect');
|
|
||||||
if (levelSelectDiv) {
|
|
||||||
levelSelectDiv.classList.add('ready');
|
|
||||||
console.log('[LevelSelector] Added "ready" class to #levelSelect');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy a default level to custom levels
|
|
||||||
*/
|
|
||||||
async function copyLevelToCustom(levelId: string): Promise<void> {
|
|
||||||
const registry = LevelRegistry.getInstance();
|
|
||||||
const customName = prompt(`Enter a name for your copy of this level:`, `${levelId}-copy`);
|
|
||||||
|
|
||||||
if (!customName || customName.trim() === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const success = await registry.copyDefaultToCustom(levelId, customName);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
alert(`Level copied as "${customName}"!`);
|
|
||||||
await populateLevelSelector(); // Refresh UI
|
|
||||||
} else {
|
|
||||||
alert('Failed to copy level. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a custom level
|
|
||||||
*/
|
|
||||||
function deleteCustomLevel(levelId: string): void {
|
|
||||||
if (!confirm(`Are you sure you want to delete "${levelId}"?`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const registry = LevelRegistry.getInstance();
|
|
||||||
const success = registry.deleteCustomLevel(levelId);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
populateLevelSelector(); // Refresh UI
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a level and dispatch event to start it
|
|
||||||
*/
|
|
||||||
export async function selectLevel(levelId: string): Promise<void> {
|
|
||||||
debugLog(`[LevelSelector] Level selected: ${levelId}`);
|
|
||||||
|
|
||||||
const registry = LevelRegistry.getInstance();
|
|
||||||
const config = await registry.getLevel(levelId);
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
console.error(`Level not found: ${levelId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save selected level
|
|
||||||
localStorage.setItem(SELECTED_LEVEL_KEY, levelId);
|
|
||||||
|
|
||||||
// Dispatch custom event that Main class will listen for
|
|
||||||
const event = new CustomEvent('levelSelected', {
|
|
||||||
detail: {levelName: levelId, config}
|
|
||||||
});
|
|
||||||
window.dispatchEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the last selected level ID
|
|
||||||
*/
|
|
||||||
export function getSelectedLevel(): string | null {
|
|
||||||
return localStorage.getItem(SELECTED_LEVEL_KEY);
|
|
||||||
}
|
|
||||||
@ -1,262 +0,0 @@
|
|||||||
import {LevelDirectory, LevelDirectoryEntry} from "../storage/levelRegistry";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracked version information for a level
|
|
||||||
*/
|
|
||||||
export interface LevelVersionInfo {
|
|
||||||
levelId: string;
|
|
||||||
loadedVersion: string;
|
|
||||||
loadedAt: Date;
|
|
||||||
manifestVersion?: string; // Latest version from directory
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version comparison result
|
|
||||||
*/
|
|
||||||
export interface VersionComparison {
|
|
||||||
levelId: string;
|
|
||||||
currentVersion: string;
|
|
||||||
latestVersion: string;
|
|
||||||
isOutdated: boolean;
|
|
||||||
changelog?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VERSION_STORAGE_KEY = 'space-game-level-versions';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages level version tracking and update detection
|
|
||||||
*/
|
|
||||||
export class LevelVersionManager {
|
|
||||||
private static instance: LevelVersionManager | null = null;
|
|
||||||
|
|
||||||
private versionMap: Map<string, LevelVersionInfo> = new Map();
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
this.loadVersions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getInstance(): LevelVersionManager {
|
|
||||||
if (!LevelVersionManager.instance) {
|
|
||||||
LevelVersionManager.instance = new LevelVersionManager();
|
|
||||||
}
|
|
||||||
return LevelVersionManager.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load version tracking from localStorage
|
|
||||||
*/
|
|
||||||
private loadVersions(): void {
|
|
||||||
const stored = localStorage.getItem(VERSION_STORAGE_KEY);
|
|
||||||
if (!stored) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const versionsArray: [string, LevelVersionInfo][] = JSON.parse(stored);
|
|
||||||
|
|
||||||
for (const [id, info] of versionsArray) {
|
|
||||||
// Parse date string back to Date object
|
|
||||||
if (info.loadedAt && typeof info.loadedAt === 'string') {
|
|
||||||
info.loadedAt = new Date(info.loadedAt);
|
|
||||||
}
|
|
||||||
this.versionMap.set(id, info);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load level versions:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save version tracking to localStorage
|
|
||||||
*/
|
|
||||||
private saveVersions(): void {
|
|
||||||
const versionsArray = Array.from(this.versionMap.entries());
|
|
||||||
localStorage.setItem(VERSION_STORAGE_KEY, JSON.stringify(versionsArray));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record that a level was loaded with a specific version
|
|
||||||
*/
|
|
||||||
public recordLevelLoaded(levelId: string, version: string): void {
|
|
||||||
const info: LevelVersionInfo = {
|
|
||||||
levelId,
|
|
||||||
loadedVersion: version,
|
|
||||||
loadedAt: new Date()
|
|
||||||
};
|
|
||||||
|
|
||||||
this.versionMap.set(levelId, info);
|
|
||||||
this.saveVersions();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update manifest versions from directory
|
|
||||||
*/
|
|
||||||
public updateManifestVersions(directory: LevelDirectory): void {
|
|
||||||
for (const entry of directory.levels) {
|
|
||||||
const existing = this.versionMap.get(entry.id);
|
|
||||||
if (existing) {
|
|
||||||
existing.manifestVersion = entry.version;
|
|
||||||
} else {
|
|
||||||
// First time seeing this level
|
|
||||||
this.versionMap.set(entry.id, {
|
|
||||||
levelId: entry.id,
|
|
||||||
loadedVersion: '', // Not yet loaded
|
|
||||||
loadedAt: new Date(),
|
|
||||||
manifestVersion: entry.version
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveVersions();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a level has an update available
|
|
||||||
*/
|
|
||||||
public hasUpdate(levelId: string): boolean {
|
|
||||||
const info = this.versionMap.get(levelId);
|
|
||||||
if (!info || !info.manifestVersion || !info.loadedVersion) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.compareVersions(info.loadedVersion, info.manifestVersion) < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get version comparison for a level
|
|
||||||
*/
|
|
||||||
public getVersionComparison(levelId: string): VersionComparison | null {
|
|
||||||
const info = this.versionMap.get(levelId);
|
|
||||||
if (!info || !info.manifestVersion) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentVersion = info.loadedVersion || '0.0';
|
|
||||||
const latestVersion = info.manifestVersion;
|
|
||||||
const isOutdated = this.compareVersions(currentVersion, latestVersion) < 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
levelId,
|
|
||||||
currentVersion,
|
|
||||||
latestVersion,
|
|
||||||
isOutdated
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all levels with available updates
|
|
||||||
*/
|
|
||||||
public getUpdatableLevels(): VersionComparison[] {
|
|
||||||
const updatable: VersionComparison[] = [];
|
|
||||||
|
|
||||||
for (const [levelId, info] of this.versionMap) {
|
|
||||||
if (info.manifestVersion && info.loadedVersion) {
|
|
||||||
const comparison = this.getVersionComparison(levelId);
|
|
||||||
if (comparison && comparison.isOutdated) {
|
|
||||||
updatable.push(comparison);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get version info for a level
|
|
||||||
*/
|
|
||||||
public getVersionInfo(levelId: string): LevelVersionInfo | undefined {
|
|
||||||
return this.versionMap.get(levelId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark a level as updated (user accepted the new version)
|
|
||||||
*/
|
|
||||||
public markAsUpdated(levelId: string, newVersion: string): void {
|
|
||||||
const info = this.versionMap.get(levelId);
|
|
||||||
if (info) {
|
|
||||||
info.loadedVersion = newVersion;
|
|
||||||
info.loadedAt = new Date();
|
|
||||||
this.saveVersions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare two semantic version strings
|
|
||||||
* Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
|
||||||
*/
|
|
||||||
private compareVersions(v1: string, v2: string): number {
|
|
||||||
const parts1 = v1.split('.').map(Number);
|
|
||||||
const parts2 = v2.split('.').map(Number);
|
|
||||||
|
|
||||||
const maxLength = Math.max(parts1.length, parts2.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < maxLength; i++) {
|
|
||||||
const part1 = parts1[i] || 0;
|
|
||||||
const part2 = parts2[i] || 0;
|
|
||||||
|
|
||||||
if (part1 < part2) return -1;
|
|
||||||
if (part1 > part2) return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all version tracking (for testing/reset)
|
|
||||||
*/
|
|
||||||
public clearAll(): void {
|
|
||||||
this.versionMap.clear();
|
|
||||||
localStorage.removeItem(VERSION_STORAGE_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get summary of version statuses
|
|
||||||
*/
|
|
||||||
public getVersionSummary(): {
|
|
||||||
total: number;
|
|
||||||
tracked: number;
|
|
||||||
updatable: number;
|
|
||||||
upToDate: number;
|
|
||||||
} {
|
|
||||||
let tracked = 0;
|
|
||||||
let updatable = 0;
|
|
||||||
let upToDate = 0;
|
|
||||||
|
|
||||||
for (const info of this.versionMap.values()) {
|
|
||||||
if (info.loadedVersion) {
|
|
||||||
tracked++;
|
|
||||||
|
|
||||||
if (info.manifestVersion) {
|
|
||||||
if (this.compareVersions(info.loadedVersion, info.manifestVersion) < 0) {
|
|
||||||
updatable++;
|
|
||||||
} else {
|
|
||||||
upToDate++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
total: this.versionMap.size,
|
|
||||||
tracked,
|
|
||||||
updatable,
|
|
||||||
upToDate
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build changelog text for version updates
|
|
||||||
*/
|
|
||||||
public static buildChangelog(directoryEntry: LevelDirectoryEntry): string {
|
|
||||||
// In the future, this could fetch from a changelog file or API
|
|
||||||
// For now, generate a simple message
|
|
||||||
return `Level updated to version ${directoryEntry.version}. Check for improvements and changes!`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is the first time loading any levels
|
|
||||||
*/
|
|
||||||
public isFirstRun(): boolean {
|
|
||||||
return this.versionMap.size === 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
138
src/main.ts
138
src/main.ts
@ -23,8 +23,6 @@ import Level from "./levels/level";
|
|||||||
import setLoadingMessage from "./utils/setLoadingMessage";
|
import setLoadingMessage from "./utils/setLoadingMessage";
|
||||||
import {RockFactory} from "./environment/asteroids/rockFactory";
|
import {RockFactory} from "./environment/asteroids/rockFactory";
|
||||||
import {ControllerDebug} from "./utils/controllerDebug";
|
import {ControllerDebug} from "./utils/controllerDebug";
|
||||||
import {router, showView} from "./core/router";
|
|
||||||
import {populateLevelSelector} from "./levels/ui/levelSelector";
|
|
||||||
import {LevelConfig} from "./levels/config/levelConfig";
|
import {LevelConfig} from "./levels/config/levelConfig";
|
||||||
import {LegacyMigration} from "./levels/migration/legacyMigration";
|
import {LegacyMigration} from "./levels/migration/legacyMigration";
|
||||||
import {LevelRegistry} from "./levels/storage/levelRegistry";
|
import {LevelRegistry} from "./levels/storage/levelRegistry";
|
||||||
@ -747,128 +745,7 @@ export class Main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup router
|
// Initialize registry and mount Svelte app
|
||||||
router.on('/', async () => {
|
|
||||||
debugLog('[Router] Home route triggered');
|
|
||||||
|
|
||||||
// Always show game view
|
|
||||||
showView('game');
|
|
||||||
debugLog('[Router] Game view shown');
|
|
||||||
|
|
||||||
// Initialize auth service (but don't block on it)
|
|
||||||
try {
|
|
||||||
const authService = AuthService.getInstance();
|
|
||||||
debugLog('[Router] Initializing auth service...');
|
|
||||||
await authService.initialize();
|
|
||||||
debugLog('[Router] Auth service initialized');
|
|
||||||
|
|
||||||
// Check if user is authenticated
|
|
||||||
const isAuthenticated = await authService.isAuthenticated();
|
|
||||||
const user = authService.getUser();
|
|
||||||
debugLog('[Router] Auth check - authenticated:', isAuthenticated, 'user:', user);
|
|
||||||
|
|
||||||
if (isAuthenticated && user) {
|
|
||||||
// User is authenticated - update profile display
|
|
||||||
debugLog('User authenticated:', user?.email || user?.name || 'Unknown');
|
|
||||||
updateUserProfile(user.name || user.email || 'Player');
|
|
||||||
} else {
|
|
||||||
// User not authenticated - show login/signup button
|
|
||||||
debugLog('User not authenticated, showing login button');
|
|
||||||
updateUserProfile(null); // This will show login button instead
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Auth failed, but allow game to continue
|
|
||||||
debugLog('Auth initialization failed, continuing without auth:', error);
|
|
||||||
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)');
|
|
||||||
await populateLevelSelector();
|
|
||||||
|
|
||||||
// Create Main instance lazily only if it doesn't exist
|
|
||||||
// But don't initialize it yet - that will happen on level selection
|
|
||||||
if (!(window as any).__mainInstance) {
|
|
||||||
debugLog('[Router] Creating Main instance (not initialized)');
|
|
||||||
const main = new Main();
|
|
||||||
(window as any).__mainInstance = main;
|
|
||||||
|
|
||||||
// Initialize demo mode without engine (just for UI purposes)
|
|
||||||
const demo = new Demo(main);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discord widget initialization with enhanced error logging
|
|
||||||
/*if (!(window as any).__discordWidget) {
|
|
||||||
debugLog('[Router] Initializing Discord widget');
|
|
||||||
const discord = new DiscordWidget();
|
|
||||||
|
|
||||||
// Initialize with your server and channel IDs
|
|
||||||
discord.initialize({
|
|
||||||
server: '1112846185913401475', // Replace with your Discord server ID
|
|
||||||
channel: '1437561367908581406', // Replace with your Discord channel ID
|
|
||||||
color: '#667eea',
|
|
||||||
glyph: ['💬', '✖️'],
|
|
||||||
notifications: true
|
|
||||||
}).then(() => {
|
|
||||||
debugLog('[Router] Discord widget ready');
|
|
||||||
(window as any).__discordWidget = discord;
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('[Router] Failed to initialize Discord widget:', error);
|
|
||||||
console.error('[Router] Error type:', error?.constructor?.name);
|
|
||||||
console.error('[Router] Error message:', error?.message);
|
|
||||||
console.error('[Router] Error stack:', error?.stack);
|
|
||||||
if (error?.response) {
|
|
||||||
console.error('[Router] GraphQL response error:', error.response);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog('[Router] Home route handler complete');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.on('/editor', () => {
|
|
||||||
showView('editor');
|
|
||||||
// Dynamically import and initialize editor
|
|
||||||
if (!(window as any).__editorInitialized) {
|
|
||||||
import('./levels/generation/levelEditor').then(() => {
|
|
||||||
(window as any).__editorInitialized = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.on('/settings', () => {
|
|
||||||
showView('settings');
|
|
||||||
// Dynamically import and initialize settings
|
|
||||||
if (!(window as any).__settingsInitialized) {
|
|
||||||
import('./ui/screens/settingsScreen').then((module) => {
|
|
||||||
module.initializeSettingsScreen();
|
|
||||||
(window as any).__settingsInitialized = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.on('/controls', () => {
|
|
||||||
showView('controls');
|
|
||||||
// Dynamically import and initialize controls screen
|
|
||||||
if (!(window as any).__controlsInitialized) {
|
|
||||||
import('./ui/screens/controlsScreen').then((module) => {
|
|
||||||
const controlsScreen = new module.ControlsScreen();
|
|
||||||
controlsScreen.initialize();
|
|
||||||
(window as any).__controlsInitialized = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize registry and start router
|
|
||||||
// This must happen BEFORE router.start() so levels are available
|
|
||||||
async function initializeApp() {
|
async function initializeApp() {
|
||||||
console.log('[Main] ========================================');
|
console.log('[Main] ========================================');
|
||||||
console.log('[Main] initializeApp() STARTED at', new Date().toISOString());
|
console.log('[Main] initializeApp() STARTED at', new Date().toISOString());
|
||||||
@ -889,8 +766,6 @@ async function initializeApp() {
|
|||||||
await LevelRegistry.getInstance().initialize();
|
await LevelRegistry.getInstance().initialize();
|
||||||
console.log('[Main] LevelRegistry.initialize() completed successfully [AFTER MIGRATION]');
|
console.log('[Main] LevelRegistry.initialize() completed successfully [AFTER MIGRATION]');
|
||||||
debugLog('[Main] LevelRegistry initialized after migration');
|
debugLog('[Main] LevelRegistry initialized after migration');
|
||||||
// NOTE: Old router disabled - now using svelte-routing
|
|
||||||
// router.start();
|
|
||||||
|
|
||||||
// Mount Svelte app
|
// Mount Svelte app
|
||||||
console.log('[Main] Mounting Svelte app [AFTER MIGRATION]');
|
console.log('[Main] Mounting Svelte app [AFTER MIGRATION]');
|
||||||
@ -917,8 +792,6 @@ async function initializeApp() {
|
|||||||
resolve();
|
resolve();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Main] Failed to initialize LevelRegistry after migration:', error);
|
console.error('[Main] Failed to initialize LevelRegistry after migration:', error);
|
||||||
// NOTE: Old router disabled - now using svelte-routing
|
|
||||||
// router.start(); // Start anyway to show error state
|
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -941,19 +814,12 @@ async function initializeApp() {
|
|||||||
if (isDev) {
|
if (isDev) {
|
||||||
(window as any).__levelRegistry = LevelRegistry.getInstance();
|
(window as any).__levelRegistry = LevelRegistry.getInstance();
|
||||||
console.log('[Main] LevelRegistry exposed to window.__levelRegistry for debugging');
|
console.log('[Main] LevelRegistry exposed to window.__levelRegistry for debugging');
|
||||||
console.log('[Main] To clear caches: window.__levelRegistry.clearAllCaches().then(() => location.reload())');
|
console.log('[Main] To clear caches: window.__levelRegistry.reset(); location.reload()');
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Old router disabled - now using svelte-routing
|
|
||||||
// console.log('[Main] About to call router.start()');
|
|
||||||
// router.start();
|
|
||||||
// console.log('[Main] router.start() completed');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Main] !!!!! EXCEPTION in LevelRegistry initialization !!!!!');
|
console.error('[Main] !!!!! EXCEPTION in LevelRegistry initialization !!!!!');
|
||||||
console.error('[Main] Failed to initialize LevelRegistry:', error);
|
console.error('[Main] Failed to initialize LevelRegistry:', error);
|
||||||
console.error('[Main] Error stack:', error?.stack);
|
console.error('[Main] Error stack:', error?.stack);
|
||||||
// NOTE: Old router disabled - now using svelte-routing
|
|
||||||
// router.start(); // Start anyway to show error state
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -104,7 +104,8 @@ export class WeaponSystem {
|
|||||||
ammoAggregate.body.setCollisionCallbackEnabled(true);
|
ammoAggregate.body.setCollisionCallbackEnabled(true);
|
||||||
|
|
||||||
// Set projectile velocity (already includes ship velocity)
|
// Set projectile velocity (already includes ship velocity)
|
||||||
ammoAggregate.body.setLinearVelocity(velocityVector);
|
// Clone to capture current direction - prevents curving if source vector updates
|
||||||
|
ammoAggregate.body.setLinearVelocity(velocityVector.clone());
|
||||||
|
|
||||||
// Consume ammo
|
// Consume ammo
|
||||||
if (this._shipStatus) {
|
if (this._shipStatus) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user