- Enable renderingGroupId=3 for missionBrief and statusScreen (always on top) - Adjust background stars: increase count to 4500, radius to 50000 - Add level editor Svelte components (AsteroidList, BaseConfig, LevelConfig, PlanetList, ShipConfig, Vector3Input editors) - Add LEVEL_TRANSITION.md documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
8.6 KiB
Immersive Level Progression Plan
Overview
Add the ability for players to progress to the next level while staying in VR/immersive mode. This includes a "NEXT LEVEL" button on the status screen, fade-to-black transition, proper cleanup, and mission brief display.
User Requirements
- Show mission brief for the new level
- Use fade-to-black transition (smoother UX)
- Reset all game statistics for each level
- Maintain deep-link reload capability
Implementation Approach
Architecture Decision: Create LevelTransitionManager
Create a new singleton class rather than adding to existing code because:
- Keeps transition logic isolated (~90 lines)
- Avoids bloating statusScreen.ts (already 700 lines)
- Single responsibility principle
- Follows existing patterns (InputControlManager, ProgressionManager)
Phase 1: Create VR Fade Effect
New file: src/ui/effects/vrFadeEffect.ts (~60 lines)
For VR, use a black sphere that surrounds the XR camera (2D post-process won't work correctly for stereo rendering):
- Create small sphere (0.5m diameter) parented to XR camera
- Material: pure black,
backFaceCulling = false(see inside) renderingGroupId = 3(render in front of everything)- Animate alpha 0→1 for fadeOut, 1→0 for fadeIn
- Use BabylonJS Animation class with 500ms duration
Phase 2: Create Level Transition Manager
New file: src/core/levelTransitionManager.ts (~90 lines)
Singleton that orchestrates the transition sequence:
transitionToLevel(nextLevelSlug):
1. Initialize fade sphere (parent to XR camera)
2. fadeOut(500ms) - screen goes black
3. currentLevel.dispose() - cleanup all entities
4. RockFactory.reset() then init() - reset asteroid factory
5. Get new level config from LevelRegistry
6. Create new Level1(config, audioEngine, false, levelSlug)
7. newLevel.initialize() - creates ship, asteroids, etc.
8. newLevel.setupXRCamera() - re-parent XR camera to new ship
9. fadeIn(500ms) - reveal new scene
10. newLevel.showMissionBrief() - show objectives
11. (Player clicks START on mission brief)
12. newLevel.startGameplay() - timer begins
Key considerations:
- Store reference to audioEngine (passed once, reused)
- Store reference to currentLevel (updated on transition)
- XR session stays active throughout
- Physics engine stays active (bodies are disposed, not engine)
Phase 3: Enable NEXT LEVEL Button on StatusScreen
File: src/ui/hud/statusScreen.ts
3a. Uncomment button code (lines 248-262)
Currently commented out NEXT LEVEL button exists. Uncomment and add hover effect:
this._nextLevelButton = Button.CreateSimpleButton("nextLevelButton", "NEXT LEVEL");
this._nextLevelButton.width = "300px";
this._nextLevelButton.height = "60px";
this._nextLevelButton.color = "white";
this._nextLevelButton.background = "#0088ff";
this._nextLevelButton.cornerRadius = 10;
this._nextLevelButton.thickness = 0;
this._nextLevelButton.fontSize = "30px";
this._nextLevelButton.fontWeight = "bold";
addButtonHoverEffect(this._nextLevelButton, "#0088ff", "#00aaff"); // ADD THIS
this._nextLevelButton.onPointerClickObservable.add(() => {
if (this._onNextLevelCallback) {
this._onNextLevelCallback();
}
});
buttonBar.addControl(this._nextLevelButton);
3b. Verify visibility logic (around line 474)
Existing logic should handle button visibility on victory:
if (this._nextLevelButton) {
this._nextLevelButton.isVisible = isGameEnded && victory && hasNextLevel;
}
Ensure hasNextLevel is properly checked using ProgressionManager.getNextLevel().
Phase 4: Modify Ship.handleNextLevel()
File: src/ship/ship.ts (lines 523-528)
Change from page reload to using LevelTransitionManager:
private async handleNextLevel(): Promise<void> {
log.debug('Next Level button clicked - transitioning to next level');
const { ProgressionManager } = await import('../game/progression');
const progression = ProgressionManager.getInstance();
const nextLevel = progression.getNextLevel();
if (nextLevel) {
const { LevelTransitionManager } = await import('../core/levelTransitionManager');
const transitionManager = LevelTransitionManager.getInstance();
await transitionManager.transitionToLevel(nextLevel);
}
}
Phase 5: Wire Up LevelTransitionManager
File: src/core/handlers/levelSelectedHandler.ts or src/main.ts
After level is created, register it with the transition manager:
import { LevelTransitionManager } from './core/levelTransitionManager';
// After level creation:
const transitionManager = LevelTransitionManager.getInstance();
transitionManager.setAudioEngine(audioEngine);
transitionManager.setCurrentLevel(level);
Cleanup Details
Gap Found: Sun, Planets, Asteroids Not Tracked
Issue: In Level1.initialize() (line 393), the comment says "sun and planets are already created by deserializer" but they are NOT stored as instance variables. This means they won't be disposed when switching levels.
Fix Required: Add new instance variables and dispose them:
// Add to Level1 class properties:
private _sun: AbstractMesh | null = null;
private _planets: AbstractMesh[] = [];
private _asteroids: AbstractMesh[] = [];
// In initialize(), store the returned entities:
this._sun = entities.sun;
this._planets = entities.planets;
this._asteroids = entities.asteroids;
// In dispose(), add cleanup:
if (this._sun) this._sun.dispose();
this._planets.forEach(p => p.dispose());
this._asteroids.forEach(a => { if (!a.isDisposed()) a.dispose(); });
What gets disposed (via Level1.dispose()):
_startBase- landing base mesh_endBase- end base mesh (if exists)_sun- NEW sun mesh_planets- NEW planet meshes array_asteroids- NEW asteroid meshes (may already be destroyed in gameplay)_backgroundStars- particle system_missionBrief- UI overlay_hintSystem- audio hints_ship- cascades to: physics body, controllers, audio, weapons, statusScreen, scoreboard_backgroundMusic- audio
What stays active:
- XR session
- XR camera (re-parented to new ship)
- Audio engine
- Main scene
- Physics engine (bodies disposed, engine stays)
- Render loop
RockFactory reset:
RockFactory.reset()clears static asteroid mesh referencesRockFactory.init()reloads base asteroid models- Ensures fresh asteroid creation for new level
Critical Files to Modify
| File | Changes |
|---|---|
src/ui/effects/vrFadeEffect.ts |
CREATE - VR fade sphere effect (~60 lines) |
src/core/levelTransitionManager.ts |
CREATE - Transition orchestration (~90 lines) |
src/ui/hud/statusScreen.ts |
MODIFY - Uncomment NEXT LEVEL button, add hover effect |
src/ship/ship.ts |
MODIFY - Update handleNextLevel() to use transition manager |
src/levels/level1.ts |
MODIFY - Add _sun, _planets, _asteroids properties and dispose them |
src/main.ts or handler |
MODIFY - Wire up transition manager on level creation |
Transition Sequence Diagram
[Victory] → StatusScreen shows NEXT LEVEL button
↓
Player clicks NEXT LEVEL
↓
Ship.handleNextLevel()
↓
LevelTransitionManager.transitionToLevel()
↓
VRFadeEffect.fadeOut(500ms) ← Screen goes black
↓
Level1.dispose() ← All entities cleaned up
↓
RockFactory.reset() + init()
↓
new Level1(newConfig) ← New level created
↓
Level1.initialize() ← Asteroids, ship, bases created
↓
Level1.setupXRCamera() ← Camera re-parented
↓
VRFadeEffect.fadeIn(500ms) ← Scene revealed
↓
Level1.showMissionBrief() ← Objectives displayed
↓
Player clicks START
↓
Level1.startGameplay() ← Timer starts, gameplay begins
Testing Checklist
- NEXT LEVEL button appears only on victory when next level exists and is unlocked
- Clicking button starts fade transition
- XR session remains active throughout
- Old level entities fully disposed (no memory leaks)
- New level loads with correct configuration
- XR camera re-parents to new ship correctly
- Mission brief displays for new level
- GameStats reset (time starts at 0:00)
- Ship status (fuel/hull/ammo) reset to full
- Deep-link reload still works (page refresh loads correct level)
- Scoreboard shows correct asteroid count
- Physics bodies cleaned up (no orphaned bodies)
- Audio continues working (background music, effects)
- Controllers work on new ship