Update rendering and add level editor components
- 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>
This commit is contained in:
parent
18a9ae9978
commit
21a6d7c9ec
265
LEVEL_TRANSITION.md
Normal file
265
LEVEL_TRANSITION.md
Normal file
@ -0,0 +1,265 @@
|
||||
# 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:
|
||||
1. Keeps transition logic isolated (~90 lines)
|
||||
2. Avoids bloating statusScreen.ts (already 700 lines)
|
||||
3. Single responsibility principle
|
||||
4. 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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
// 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 references
|
||||
- `RockFactory.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
|
||||
|
||||
1. NEXT LEVEL button appears only on victory when next level exists and is unlocked
|
||||
2. Clicking button starts fade transition
|
||||
3. XR session remains active throughout
|
||||
4. Old level entities fully disposed (no memory leaks)
|
||||
5. New level loads with correct configuration
|
||||
6. XR camera re-parents to new ship correctly
|
||||
7. Mission brief displays for new level
|
||||
8. GameStats reset (time starts at 0:00)
|
||||
9. Ship status (fuel/hull/ammo) reset to full
|
||||
10. Deep-link reload still works (page refresh loads correct level)
|
||||
11. Scoreboard shows correct asteroid count
|
||||
12. Physics bodies cleaned up (no orphaned bodies)
|
||||
13. Audio continues working (background music, effects)
|
||||
14. Controllers work on new ship
|
||||
173
src/components/editor/AsteroidListEditor.svelte
Normal file
173
src/components/editor/AsteroidListEditor.svelte
Normal file
@ -0,0 +1,173 @@
|
||||
<script lang="ts">
|
||||
import type { AsteroidConfig } from '../../levels/config/levelConfig';
|
||||
import Button from '../shared/Button.svelte';
|
||||
import Section from '../shared/Section.svelte';
|
||||
import Vector3Input from './Vector3Input.svelte';
|
||||
import NumberInput from '../shared/NumberInput.svelte';
|
||||
import FormGroup from '../shared/FormGroup.svelte';
|
||||
|
||||
export let asteroids: AsteroidConfig[] = [];
|
||||
|
||||
let editingIndex: number | null = null;
|
||||
let editingAsteroid: AsteroidConfig | null = null;
|
||||
|
||||
function handleAdd() {
|
||||
const newId = `ast_${Date.now()}`;
|
||||
const newAsteroid: AsteroidConfig = {
|
||||
id: newId,
|
||||
position: [0, 0, 100],
|
||||
scale: 10,
|
||||
linearVelocity: [0, 0, 0],
|
||||
angularVelocity: [0, 0, 0]
|
||||
};
|
||||
asteroids = [...asteroids, newAsteroid];
|
||||
editingIndex = asteroids.length - 1;
|
||||
editingAsteroid = { ...newAsteroid };
|
||||
}
|
||||
|
||||
function handleEdit(index: number) {
|
||||
editingIndex = index;
|
||||
editingAsteroid = { ...asteroids[index] };
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
if (editingIndex !== null && editingAsteroid) {
|
||||
asteroids[editingIndex] = editingAsteroid;
|
||||
asteroids = asteroids; // trigger reactivity
|
||||
}
|
||||
editingIndex = null;
|
||||
editingAsteroid = null;
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
editingIndex = null;
|
||||
editingAsteroid = null;
|
||||
}
|
||||
|
||||
function handleDelete(index: number) {
|
||||
if (confirm('Delete this asteroid?')) {
|
||||
asteroids = asteroids.filter((_, i) => i !== index);
|
||||
if (editingIndex === index) {
|
||||
editingIndex = null;
|
||||
editingAsteroid = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatPosition(pos: [number, number, number]): string {
|
||||
return `${pos[0]}, ${pos[1]}, ${pos[2]}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Section title="☄️ Asteroids ({asteroids.length})">
|
||||
<div class="asteroid-header">
|
||||
<Button variant="primary" on:click={handleAdd}>+ Add Asteroid</Button>
|
||||
</div>
|
||||
|
||||
{#if editingAsteroid !== null}
|
||||
<div class="edit-form">
|
||||
<h4>Edit Asteroid: {editingAsteroid.id}</h4>
|
||||
<FormGroup label="ID">
|
||||
<input type="text" class="settings-input" bind:value={editingAsteroid.id} />
|
||||
</FormGroup>
|
||||
<Vector3Input label="Position" bind:value={editingAsteroid.position} step={10} />
|
||||
<FormGroup label="Scale">
|
||||
<NumberInput bind:value={editingAsteroid.scale} min={1} max={100} step={1} />
|
||||
</FormGroup>
|
||||
<Vector3Input label="Linear Velocity" bind:value={editingAsteroid.linearVelocity} step={1} />
|
||||
<Vector3Input label="Angular Velocity" bind:value={editingAsteroid.angularVelocity} step={0.1} />
|
||||
<div class="edit-actions">
|
||||
<Button variant="primary" on:click={handleSave}>Save</Button>
|
||||
<Button variant="secondary" on:click={handleCancel}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if asteroids.length === 0}
|
||||
<p class="empty-message">No asteroids configured.</p>
|
||||
{:else}
|
||||
<div class="asteroid-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Position</th>
|
||||
<th>Scale</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each asteroids as asteroid, index}
|
||||
<tr class:editing={editingIndex === index}>
|
||||
<td>{asteroid.id}</td>
|
||||
<td>{formatPosition(asteroid.position)}</td>
|
||||
<td>{asteroid.scale}</td>
|
||||
<td class="actions">
|
||||
<Button variant="secondary" on:click={() => handleEdit(index)}>Edit</Button>
|
||||
<Button variant="danger" on:click={() => handleDelete(index)}>×</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</Section>
|
||||
|
||||
<style>
|
||||
.asteroid-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
background: var(--color-bg-secondary, #1a1a2e);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.edit-form h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--color-text-primary, #fff);
|
||||
}
|
||||
|
||||
.edit-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.asteroid-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--color-border, #333);
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--color-bg-secondary, #1a1a2e);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr.editing {
|
||||
background: var(--color-bg-hover, #252540);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
color: var(--color-text-secondary, #888);
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
42
src/components/editor/BaseConfigEditor.svelte
Normal file
42
src/components/editor/BaseConfigEditor.svelte
Normal file
@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import type { StartBaseConfig } from '../../levels/config/levelConfig';
|
||||
import Vector3Input from './Vector3Input.svelte';
|
||||
import Section from '../shared/Section.svelte';
|
||||
import Checkbox from '../shared/Checkbox.svelte';
|
||||
|
||||
export let config: StartBaseConfig | undefined;
|
||||
export let onToggle: (enabled: boolean) => void;
|
||||
|
||||
let enabled = !!config;
|
||||
|
||||
function handleToggle() {
|
||||
enabled = !enabled;
|
||||
onToggle(enabled);
|
||||
}
|
||||
|
||||
// Ensure position exists
|
||||
$: if (config && !config.position) config.position = [0, 0, 0];
|
||||
</script>
|
||||
|
||||
<Section title="🛬 Start Base Configuration">
|
||||
<div class="base-toggle">
|
||||
<Checkbox checked={enabled} label="Enable Start Base" on:change={handleToggle} />
|
||||
</div>
|
||||
|
||||
{#if enabled && config}
|
||||
<Vector3Input label="Position" bind:value={config.position} step={10} />
|
||||
{:else}
|
||||
<p class="disabled-message">Start base is disabled for this level.</p>
|
||||
{/if}
|
||||
</Section>
|
||||
|
||||
<style>
|
||||
.base-toggle {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.disabled-message {
|
||||
color: var(--color-text-secondary, #888);
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
208
src/components/editor/LevelConfigEditor.svelte
Normal file
208
src/components/editor/LevelConfigEditor.svelte
Normal file
@ -0,0 +1,208 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Link, navigate } from 'svelte-routing';
|
||||
import { CloudLevelService, type CloudLevelEntry } from '../../services/cloudLevelService';
|
||||
import type { LevelConfig, StartBaseConfig } from '../../levels/config/levelConfig';
|
||||
import Button from '../shared/Button.svelte';
|
||||
import Section from '../shared/Section.svelte';
|
||||
import InfoBox from '../shared/InfoBox.svelte';
|
||||
import ShipConfigEditor from './ShipConfigEditor.svelte';
|
||||
import BaseConfigEditor from './BaseConfigEditor.svelte';
|
||||
import AsteroidListEditor from './AsteroidListEditor.svelte';
|
||||
import PlanetListEditor from './PlanetListEditor.svelte';
|
||||
|
||||
export let levelId: string = '';
|
||||
|
||||
let isLoading = true;
|
||||
let isAuthorized = false;
|
||||
let isSaving = false;
|
||||
let level: CloudLevelEntry | null = null;
|
||||
let config: LevelConfig | null = null;
|
||||
let error = '';
|
||||
let activeTab = 'ship';
|
||||
|
||||
// Message state
|
||||
let message = '';
|
||||
let messageType: 'success' | 'error' | 'warning' = 'success';
|
||||
let showMessage = false;
|
||||
|
||||
const tabs = [
|
||||
{ id: 'ship', label: '🚀 Ship' },
|
||||
{ id: 'base', label: '🛬 Base' },
|
||||
{ id: 'asteroids', label: '☄️ Asteroids' },
|
||||
{ id: 'planets', label: '🪐 Planets' }
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
await loadLevel();
|
||||
});
|
||||
|
||||
async function loadLevel() {
|
||||
isLoading = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
const service = CloudLevelService.getInstance();
|
||||
const permissions = await service.getAdminPermissions();
|
||||
|
||||
if (!permissions?.canManageOfficial) {
|
||||
isAuthorized = false;
|
||||
isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isAuthorized = true;
|
||||
level = await service.getLevelById(levelId);
|
||||
|
||||
if (!level) {
|
||||
error = 'Level not found';
|
||||
isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Deep clone config to avoid mutating original
|
||||
config = JSON.parse(JSON.stringify(level.config));
|
||||
} catch (err) {
|
||||
error = 'Failed to load level';
|
||||
console.error('[LevelConfigEditor] Error:', err);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!level || !config) return;
|
||||
|
||||
isSaving = true;
|
||||
showMessage = false;
|
||||
|
||||
try {
|
||||
const service = CloudLevelService.getInstance();
|
||||
const updated = await service.updateLevelAsAdmin(level.id, { config });
|
||||
|
||||
if (updated) {
|
||||
level = updated;
|
||||
message = 'Config saved successfully!';
|
||||
messageType = 'success';
|
||||
} else {
|
||||
message = 'Failed to save config';
|
||||
messageType = 'error';
|
||||
}
|
||||
} catch (err) {
|
||||
message = 'Error saving config';
|
||||
messageType = 'error';
|
||||
console.error('[LevelConfigEditor] Save error:', err);
|
||||
} finally {
|
||||
isSaving = false;
|
||||
showMessage = true;
|
||||
setTimeout(() => { showMessage = false; }, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
navigate(`/editor/${levelId}`);
|
||||
}
|
||||
|
||||
function handleBaseToggle(enabled: boolean) {
|
||||
if (!config) return;
|
||||
if (enabled && !config.startBase) {
|
||||
config.startBase = { position: [0, 0, 0] };
|
||||
} else if (!enabled) {
|
||||
config.startBase = undefined;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="editor-container">
|
||||
<Link to="/editor/{levelId}" class="back-link">← Back to Level Details</Link>
|
||||
|
||||
<h1>⚙️ Edit Config</h1>
|
||||
|
||||
{#if isLoading}
|
||||
<Section title="Loading...">
|
||||
<p>Loading level configuration...</p>
|
||||
</Section>
|
||||
{:else if !isAuthorized}
|
||||
<Section title="🚫 Access Denied">
|
||||
<p>You do not have permission to edit level configs.</p>
|
||||
</Section>
|
||||
{:else if error}
|
||||
<Section title="❌ Error">
|
||||
<p>{error}</p>
|
||||
<Button variant="secondary" on:click={() => navigate('/editor')}>Back to List</Button>
|
||||
</Section>
|
||||
{:else if level && config}
|
||||
<p class="subtitle">Level: {level.name}</p>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="tabs">
|
||||
{#each tabs as tab}
|
||||
<button
|
||||
class="tab"
|
||||
class:active={activeTab === tab.id}
|
||||
on:click={() => activeTab = tab.id}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content">
|
||||
{#if activeTab === 'ship'}
|
||||
<ShipConfigEditor bind:config={config.ship} />
|
||||
{:else if activeTab === 'base'}
|
||||
<BaseConfigEditor config={config.startBase} onToggle={handleBaseToggle} />
|
||||
{:else if activeTab === 'asteroids'}
|
||||
<AsteroidListEditor bind:asteroids={config.asteroids} />
|
||||
{:else if activeTab === 'planets'}
|
||||
<PlanetListEditor bind:planets={config.planets} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<Button variant="primary" on:click={handleSave} disabled={isSaving}>
|
||||
{isSaving ? 'Saving...' : '💾 Save Config'}
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={handleCancel} disabled={isSaving}>Cancel</Button>
|
||||
</div>
|
||||
|
||||
<InfoBox {message} type={messageType} visible={showMessage} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 2px solid var(--color-border, #333);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 0.5rem 1rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--color-text-secondary, #888);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
border-radius: 4px 4px 0 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--color-text-primary, #fff);
|
||||
background: var(--color-bg-hover, #252540);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--color-primary, #4a9eff);
|
||||
background: var(--color-bg-secondary, #1a1a2e);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
min-height: 300px;
|
||||
}
|
||||
</style>
|
||||
@ -211,6 +211,12 @@
|
||||
<textarea class="settings-textarea mission-brief" bind:value={missionBriefText} rows="6"
|
||||
placeholder="Welcome to your first mission... Navigate through the asteroid field..."></textarea>
|
||||
</Section>
|
||||
|
||||
<Section title="Level Configuration" description="Edit ship, base, asteroids, and planets">
|
||||
<Button variant="secondary" on:click={() => navigate(`/editor/${levelId}/config`)}>
|
||||
⚙️ Edit Config (Ship, Asteroids, Planets...)
|
||||
</Button>
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
|
||||
263
src/components/editor/PlanetListEditor.svelte
Normal file
263
src/components/editor/PlanetListEditor.svelte
Normal file
@ -0,0 +1,263 @@
|
||||
<script lang="ts">
|
||||
import type { PlanetConfig } from '../../levels/config/levelConfig';
|
||||
import Button from '../shared/Button.svelte';
|
||||
import Section from '../shared/Section.svelte';
|
||||
import Vector3Input from './Vector3Input.svelte';
|
||||
import NumberInput from '../shared/NumberInput.svelte';
|
||||
import FormGroup from '../shared/FormGroup.svelte';
|
||||
import Select from '../shared/Select.svelte';
|
||||
|
||||
export let planets: PlanetConfig[] = [];
|
||||
|
||||
const textureOptions = [
|
||||
{ value: '', label: '(None)' },
|
||||
// Arid
|
||||
{ value: '/assets/materials/planetTextures/Arid/Arid_01-512x512.png', label: 'Arid 01' },
|
||||
{ value: '/assets/materials/planetTextures/Arid/Arid_02-512x512.png', label: 'Arid 02' },
|
||||
{ value: '/assets/materials/planetTextures/Arid/Arid_03-512x512.png', label: 'Arid 03' },
|
||||
{ value: '/assets/materials/planetTextures/Arid/Arid_04-512x512.png', label: 'Arid 04' },
|
||||
{ value: '/assets/materials/planetTextures/Arid/Arid_05-512x512.png', label: 'Arid 05' },
|
||||
// Barren
|
||||
{ value: '/assets/materials/planetTextures/Barren/Barren_01-512x512.png', label: 'Barren 01' },
|
||||
{ value: '/assets/materials/planetTextures/Barren/Barren_02-512x512.png', label: 'Barren 02' },
|
||||
{ value: '/assets/materials/planetTextures/Barren/Barren_03-512x512.png', label: 'Barren 03' },
|
||||
{ value: '/assets/materials/planetTextures/Barren/Barren_04-512x512.png', label: 'Barren 04' },
|
||||
{ value: '/assets/materials/planetTextures/Barren/Barren_05-512x512.png', label: 'Barren 05' },
|
||||
// Dusty
|
||||
{ value: '/assets/materials/planetTextures/Dusty/Dusty_01-512x512.png', label: 'Dusty 01' },
|
||||
{ value: '/assets/materials/planetTextures/Dusty/Dusty_02-512x512.png', label: 'Dusty 02' },
|
||||
{ value: '/assets/materials/planetTextures/Dusty/Dusty_03-512x512.png', label: 'Dusty 03' },
|
||||
{ value: '/assets/materials/planetTextures/Dusty/Dusty_04-512x512.png', label: 'Dusty 04' },
|
||||
{ value: '/assets/materials/planetTextures/Dusty/Dusty_05-512x512.png', label: 'Dusty 05' },
|
||||
// Gaseous
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_01-512x512.png', label: 'Gaseous 01' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_02-512x512.png', label: 'Gaseous 02' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_03-512x512.png', label: 'Gaseous 03' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_04-512x512.png', label: 'Gaseous 04' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_05-512x512.png', label: 'Gaseous 05' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_06-512x512.png', label: 'Gaseous 06' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_07-512x512.png', label: 'Gaseous 07' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_08-512x512.png', label: 'Gaseous 08' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_09-512x512.png', label: 'Gaseous 09' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_10-512x512.png', label: 'Gaseous 10' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_11-512x512.png', label: 'Gaseous 11' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_12-512x512.png', label: 'Gaseous 12' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_13-512x512.png', label: 'Gaseous 13' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_14-512x512.png', label: 'Gaseous 14' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_15-512x512.png', label: 'Gaseous 15' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_16-512x512.png', label: 'Gaseous 16' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_17-512x512.png', label: 'Gaseous 17' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_18-512x512.png', label: 'Gaseous 18' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_19-512x512.png', label: 'Gaseous 19' },
|
||||
{ value: '/assets/materials/planetTextures/Gaseous/Gaseous_20-512x512.png', label: 'Gaseous 20' },
|
||||
// Grassland
|
||||
{ value: '/assets/materials/planetTextures/Grassland/Grassland_01-512x512.png', label: 'Grassland 01' },
|
||||
{ value: '/assets/materials/planetTextures/Grassland/Grassland_02-512x512.png', label: 'Grassland 02' },
|
||||
{ value: '/assets/materials/planetTextures/Grassland/Grassland_03-512x512.png', label: 'Grassland 03' },
|
||||
{ value: '/assets/materials/planetTextures/Grassland/Grassland_04-512x512.png', label: 'Grassland 04' },
|
||||
{ value: '/assets/materials/planetTextures/Grassland/Grassland_05-512x512.png', label: 'Grassland 05' },
|
||||
// Jungle
|
||||
{ value: '/assets/materials/planetTextures/Jungle/Jungle_01-512x512.png', label: 'Jungle 01' },
|
||||
{ value: '/assets/materials/planetTextures/Jungle/Jungle_02-512x512.png', label: 'Jungle 02' },
|
||||
{ value: '/assets/materials/planetTextures/Jungle/Jungle_03-512x512.png', label: 'Jungle 03' },
|
||||
{ value: '/assets/materials/planetTextures/Jungle/Jungle_04-512x512.png', label: 'Jungle 04' },
|
||||
{ value: '/assets/materials/planetTextures/Jungle/Jungle_05-512x512.png', label: 'Jungle 05' },
|
||||
// Marshy
|
||||
{ value: '/assets/materials/planetTextures/Marshy/Marshy_01-512x512.png', label: 'Marshy 01' },
|
||||
{ value: '/assets/materials/planetTextures/Marshy/Marshy_02-512x512.png', label: 'Marshy 02' },
|
||||
{ value: '/assets/materials/planetTextures/Marshy/Marshy_03-512x512.png', label: 'Marshy 03' },
|
||||
{ value: '/assets/materials/planetTextures/Marshy/Marshy_04-512x512.png', label: 'Marshy 04' },
|
||||
{ value: '/assets/materials/planetTextures/Marshy/Marshy_05-512x512.png', label: 'Marshy 05' },
|
||||
// Martian
|
||||
{ value: '/assets/materials/planetTextures/Martian/Martian_01-512x512.png', label: 'Martian 01' },
|
||||
{ value: '/assets/materials/planetTextures/Martian/Martian_02-512x512.png', label: 'Martian 02' },
|
||||
{ value: '/assets/materials/planetTextures/Martian/Martian_03-512x512.png', label: 'Martian 03' },
|
||||
{ value: '/assets/materials/planetTextures/Martian/Martian_04-512x512.png', label: 'Martian 04' },
|
||||
{ value: '/assets/materials/planetTextures/Martian/Martian_05-512x512.png', label: 'Martian 05' },
|
||||
// Methane
|
||||
{ value: '/assets/materials/planetTextures/Methane/Methane_01-512x512.png', label: 'Methane 01' },
|
||||
{ value: '/assets/materials/planetTextures/Methane/Methane_02-512x512.png', label: 'Methane 02' },
|
||||
{ value: '/assets/materials/planetTextures/Methane/Methane_03-512x512.png', label: 'Methane 03' },
|
||||
{ value: '/assets/materials/planetTextures/Methane/Methane_04-512x512.png', label: 'Methane 04' },
|
||||
{ value: '/assets/materials/planetTextures/Methane/Methane_05-512x512.png', label: 'Methane 05' },
|
||||
// Sandy
|
||||
{ value: '/assets/materials/planetTextures/Sandy/Sandy_01-512x512.png', label: 'Sandy 01' },
|
||||
{ value: '/assets/materials/planetTextures/Sandy/Sandy_02-512x512.png', label: 'Sandy 02' },
|
||||
{ value: '/assets/materials/planetTextures/Sandy/Sandy_03-512x512.png', label: 'Sandy 03' },
|
||||
{ value: '/assets/materials/planetTextures/Sandy/Sandy_04-512x512.png', label: 'Sandy 04' },
|
||||
{ value: '/assets/materials/planetTextures/Sandy/Sandy_05-512x512.png', label: 'Sandy 05' },
|
||||
// Snowy
|
||||
{ value: '/assets/materials/planetTextures/Snowy/Snowy_01-512x512.png', label: 'Snowy 01' },
|
||||
{ value: '/assets/materials/planetTextures/Snowy/Snowy_02-512x512.png', label: 'Snowy 02' },
|
||||
{ value: '/assets/materials/planetTextures/Snowy/Snowy_03-512x512.png', label: 'Snowy 03' },
|
||||
{ value: '/assets/materials/planetTextures/Snowy/Snowy_04-512x512.png', label: 'Snowy 04' },
|
||||
{ value: '/assets/materials/planetTextures/Snowy/Snowy_05-512x512.png', label: 'Snowy 05' },
|
||||
// Tundra
|
||||
{ value: '/assets/materials/planetTextures/Tundra/Tundra_01-512x512.png', label: 'Tundra 01' },
|
||||
{ value: '/assets/materials/planetTextures/Tundra/Tundra_02-512x512.png', label: 'Tundra 02' },
|
||||
{ value: '/assets/materials/planetTextures/Tundra/Tundra_03-512x512.png', label: 'Tundra 03' },
|
||||
{ value: '/assets/materials/planetTextures/Tundra/Tundra_04-512x512.png', label: 'Tundra 04' },
|
||||
{ value: '/assets/materials/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-512x512.png', label: 'Tundra 05' }
|
||||
];
|
||||
|
||||
let editingIndex: number | null = null;
|
||||
let editingPlanet: PlanetConfig | null = null;
|
||||
|
||||
function handleAdd() {
|
||||
const newPlanet: PlanetConfig = {
|
||||
name: `Planet ${planets.length + 1}`,
|
||||
position: [0, 0, 500],
|
||||
diameter: 100
|
||||
};
|
||||
planets = [...planets, newPlanet];
|
||||
editingIndex = planets.length - 1;
|
||||
editingPlanet = { ...newPlanet };
|
||||
}
|
||||
|
||||
function handleEdit(index: number) {
|
||||
editingIndex = index;
|
||||
editingPlanet = { ...planets[index] };
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
if (editingIndex !== null && editingPlanet) {
|
||||
planets[editingIndex] = editingPlanet;
|
||||
planets = planets; // trigger reactivity
|
||||
}
|
||||
editingIndex = null;
|
||||
editingPlanet = null;
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
editingIndex = null;
|
||||
editingPlanet = null;
|
||||
}
|
||||
|
||||
function handleDelete(index: number) {
|
||||
if (confirm('Delete this planet?')) {
|
||||
planets = planets.filter((_, i) => i !== index);
|
||||
if (editingIndex === index) {
|
||||
editingIndex = null;
|
||||
editingPlanet = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatPosition(pos: [number, number, number]): string {
|
||||
return `${pos[0]}, ${pos[1]}, ${pos[2]}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Section title="🪐 Planets ({planets.length})">
|
||||
<div class="planet-header">
|
||||
<Button variant="primary" on:click={handleAdd}>+ Add Planet</Button>
|
||||
</div>
|
||||
|
||||
{#if editingPlanet !== null}
|
||||
<div class="edit-form">
|
||||
<h4>Edit Planet: {editingPlanet.name}</h4>
|
||||
<FormGroup label="Name">
|
||||
<input type="text" class="settings-input" bind:value={editingPlanet.name} />
|
||||
</FormGroup>
|
||||
<Vector3Input label="Position" bind:value={editingPlanet.position} step={50} />
|
||||
<FormGroup label="Diameter">
|
||||
<NumberInput bind:value={editingPlanet.diameter} min={10} max={10000} step={10} />
|
||||
</FormGroup>
|
||||
<FormGroup label="Texture">
|
||||
<Select bind:value={editingPlanet.texturePath} options={textureOptions} />
|
||||
</FormGroup>
|
||||
<div class="edit-actions">
|
||||
<Button variant="primary" on:click={handleSave}>Save</Button>
|
||||
<Button variant="secondary" on:click={handleCancel}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if planets.length === 0}
|
||||
<p class="empty-message">No planets configured.</p>
|
||||
{:else}
|
||||
<div class="planet-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Position</th>
|
||||
<th>Diameter</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each planets as planet, index}
|
||||
<tr class:editing={editingIndex === index}>
|
||||
<td>{planet.name}</td>
|
||||
<td>{formatPosition(planet.position)}</td>
|
||||
<td>{planet.diameter}</td>
|
||||
<td class="actions">
|
||||
<Button variant="secondary" on:click={() => handleEdit(index)}>Edit</Button>
|
||||
<Button variant="danger" on:click={() => handleDelete(index)}>×</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</Section>
|
||||
|
||||
<style>
|
||||
.planet-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
background: var(--color-bg-secondary, #1a1a2e);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.edit-form h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--color-text-primary, #fff);
|
||||
}
|
||||
|
||||
.edit-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.planet-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--color-border, #333);
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--color-bg-secondary, #1a1a2e);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr.editing {
|
||||
background: var(--color-bg-hover, #252540);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
color: var(--color-text-secondary, #888);
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
18
src/components/editor/ShipConfigEditor.svelte
Normal file
18
src/components/editor/ShipConfigEditor.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { ShipConfig } from '../../levels/config/levelConfig';
|
||||
import Vector3Input from './Vector3Input.svelte';
|
||||
import Section from '../shared/Section.svelte';
|
||||
|
||||
export let config: ShipConfig;
|
||||
|
||||
// Ensure arrays exist with defaults
|
||||
$: if (!config.position) config.position = [0, 0, 0];
|
||||
$: if (!config.linearVelocity) config.linearVelocity = [0, 0, 0];
|
||||
$: if (!config.angularVelocity) config.angularVelocity = [0, 0, 0];
|
||||
</script>
|
||||
|
||||
<Section title="🚀 Ship Configuration">
|
||||
<Vector3Input label="Position" bind:value={config.position} step={10} />
|
||||
<Vector3Input label="Linear Velocity" bind:value={config.linearVelocity} step={1} />
|
||||
<Vector3Input label="Angular Velocity" bind:value={config.angularVelocity} step={0.1} />
|
||||
</Section>
|
||||
65
src/components/editor/Vector3Input.svelte
Normal file
65
src/components/editor/Vector3Input.svelte
Normal file
@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
export let label: string = '';
|
||||
export let value: [number, number, number] = [0, 0, 0];
|
||||
export let step: number = 1;
|
||||
|
||||
// Ensure value is always a valid array
|
||||
$: if (!value || value.length !== 3) {
|
||||
value = [0, 0, 0];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="vector3-input">
|
||||
{#if label}
|
||||
<label class="vector3-label">{label}</label>
|
||||
{/if}
|
||||
<div class="vector3-fields">
|
||||
<div class="vector3-field">
|
||||
<span class="axis-label">X</span>
|
||||
<input type="number" bind:value={value[0]} {step} class="settings-input" />
|
||||
</div>
|
||||
<div class="vector3-field">
|
||||
<span class="axis-label">Y</span>
|
||||
<input type="number" bind:value={value[1]} {step} class="settings-input" />
|
||||
</div>
|
||||
<div class="vector3-field">
|
||||
<span class="axis-label">Z</span>
|
||||
<input type="number" bind:value={value[2]} {step} class="settings-input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.vector3-input {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.vector3-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary, #fff);
|
||||
}
|
||||
|
||||
.vector3-fields {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.vector3-field {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.axis-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-secondary, #aaa);
|
||||
min-width: 1rem;
|
||||
}
|
||||
|
||||
.vector3-field input {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -12,6 +12,7 @@
|
||||
import PlayLevel from '../game/PlayLevel.svelte';
|
||||
import LevelEditor from '../editor/LevelEditor.svelte';
|
||||
import LevelEditForm from '../editor/LevelEditForm.svelte';
|
||||
import LevelConfigEditor from '../editor/LevelConfigEditor.svelte';
|
||||
import SettingsScreen from '../settings/SettingsScreen.svelte';
|
||||
import ControlsScreen from '../controls/ControlsScreen.svelte';
|
||||
import Leaderboard from '../leaderboard/Leaderboard.svelte';
|
||||
@ -49,6 +50,9 @@
|
||||
<Route path="/editor/:levelId" let:params>
|
||||
<LevelEditForm levelId={params.levelId} />
|
||||
</Route>
|
||||
<Route path="/editor/:levelId/config" let:params>
|
||||
<LevelConfigEditor levelId={params.levelId} />
|
||||
</Route>
|
||||
<Route path="/settings"><SettingsScreen /></Route>
|
||||
<Route path="/controls"><ControlsScreen /></Route>
|
||||
<Route path="/leaderboard"><Leaderboard /></Route>
|
||||
|
||||
@ -29,9 +29,9 @@ export class BackgroundStars {
|
||||
|
||||
// Default configuration (reduced from 5000 for Quest 2 performance)
|
||||
private static readonly DEFAULT_CONFIG: Required<BackgroundStarsConfig> = {
|
||||
count: 2500,
|
||||
radius: 5000,
|
||||
minBrightness: 0.3,
|
||||
count: 4500,
|
||||
radius: 50000,
|
||||
minBrightness: 0.1,
|
||||
maxBrightness: 1.0,
|
||||
pointSize: .1,
|
||||
colors: [
|
||||
|
||||
@ -51,7 +51,7 @@ export class MissionBrief {
|
||||
mesh.parent = ship;
|
||||
mesh.position = new Vector3(0,1.2,2);
|
||||
mesh.rotation = new Vector3(0, 0, 0);
|
||||
//mesh.renderingGroupId = 3; // Same as status screen for consistent rendering
|
||||
mesh.renderingGroupId = 3; // Same as status screen for consistent rendering
|
||||
mesh.metadata = { uiPickable: true }; // TAG: VR UI - allow pointer selection
|
||||
log.info('[MissionBrief] Mesh parented to ship at position:', mesh.position);
|
||||
log.info('[MissionBrief] Mesh absolute position:', mesh.getAbsolutePosition());
|
||||
|
||||
@ -101,7 +101,7 @@ export class StatusScreen {
|
||||
// Parent to ship for fixed cockpit position
|
||||
this._screenMesh.parent = this._shipNode;
|
||||
this._screenMesh.position = new Vector3(0, 1.1, 2); // 2 meters forward in local space
|
||||
//this._screenMesh.renderingGroupId = 3; // Always render on top
|
||||
this._screenMesh.renderingGroupId = 3; // Always render on top
|
||||
this._screenMesh.metadata = { uiPickable: true }; // TAG: VR UI - allow pointer selection
|
||||
|
||||
// Create material
|
||||
|
||||
Loading…
Reference in New Issue
Block a user