space-game/index.html
Michael Mainguy ff8d69b6ec
All checks were successful
Build / build (push) Successful in 1m32s
Add VR controller remapping configuration system
Implement comprehensive controller remapping UI that allows users to customize
VR controller button and stick mappings with per-axis granularity and inversion
controls. Configuration persists to localStorage and applies on level start.

## Features
- Full-page controller remapping UI at #/controls
- Per-axis stick mapping (4 dropdowns: leftX, leftY, rightX, rightY)
- Individual axis inversion toggles (8 total invert options)
- Button remapping (6 buttons: trigger, A, B, X, Y, squeeze)
- Available actions: yaw, pitch, roll, forward thrust, camera, status screen
- Configuration validation with warnings for duplicates/missing controls
- Preview/test functionality to review current mapping
- Reset to default option
- localStorage persistence with backward compatibility

## Implementation
- ControllerMappingConfig singleton manages configuration and validation
- ControlsScreen handles UI logic and form manipulation
- ControllerInput applies mapping by translating raw input to actions
- Actions mapped back to virtual stick positions for ShipPhysics
- No changes needed to ShipPhysics - receives correctly mapped values

## User Flow
1. Navigate to Controls via header menu
2. Select action for each stick axis (yaw/pitch/roll/forward/none)
3. Toggle invert checkboxes as needed
4. Assign button actions (fire/camera/status/none)
5. Save configuration
6. Changes apply when starting new level

## Technical Details
- Storage key: 'space-game-controller-mapping'
- Raw stick values stored, mapping applied in getInputState()
- Supports future actions without code changes
- Validation ensures critical controls (fire, forward) are mapped

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 08:11:04 -06:00

565 lines
23 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1, height=device-height" name="viewport">
<link href="/styles.css" rel="stylesheet">
<title>Space Game</title>
<script>
navigator.serviceWorker.getRegistrations().then(registrations => {
for (const registration of registrations) {
registration.unregister();
}
});
</script>
</head>
<body>
<!-- Game View -->
<div data-view="game">
<canvas id="gameCanvas"></canvas>
<!-- Semantic Header with Navigation -->
<header class="app-header" id="appHeader" style="display: none;">
<div class="header-content">
<div class="header-left">
<h1 class="app-title">Space Combat VR</h1>
</div>
<nav class="header-nav">
<div id="userProfile"></div>
<a href="#/editor" class="nav-link editor-link">📝 Level Editor</a>
<a href="#/controls" class="nav-link controls-link">🎮 Controls</a>
<a href="#/settings" class="nav-link settings-link">⚙️ Settings</a>
</nav>
</div>
</header>
<div id="mainDiv">
<div id="loadingDiv"></div>
<div id="levelSelect">
<!-- Hero Section -->
<div class="hero">
<h1 class="hero-title">🚀 Space Combat VR</h1>
<p class="hero-subtitle">
Pilot your spaceship through asteroid fields and complete missions
</p>
</div>
<!-- Level Selection Section -->
<div class="level-section">
<h2 class="level-header">Your Mission</h2>
<p class="level-description">
Complete levels to unlock new challenges and the level editor
</p>
<div id="levelCardsContainer" class="card-container">
<!-- Level cards will be dynamically populated from localStorage -->
</div>
</div>
<!-- Controls Section (Collapsed by default) -->
<details class="controls-info">
<summary>
🎮 How to Play (Click to expand)
</summary>
<div class="controls-grid">
<div class="control-section">
<h3>VR Controllers (Required for VR)</h3>
<ul>
<li><strong>Left Thumbstick:</strong> Move forward/backward and yaw left/right</li>
<li><strong>Right Thumbstick:</strong> Pitch up/down and Roll left/right</li>
<li><strong>Front Trigger:</strong> Fire weapon</li>
</ul>
</div>
<div class="control-section">
<h3>Desktop Controls (Preview Mode)</h3>
<ul>
<li><strong>W/S:</strong> Move forward/backward</li>
<li><strong>A/D:</strong> Yaw left/right</li>
<li><strong>Arrow Up/Down:</strong> Pitch up/down</li>
<li><strong>Arrow Left/Right:</strong> Roll left/right</li>
<li><strong>Space:</strong> Fire weapon</li>
</ul>
</div>
</div>
<p class="controls-note">
⚠️ <strong>Note:</strong> This game is designed for VR headsets with controllers. Desktop controls are provided for preview and testing purposes only.
</p>
</details>
<!-- Test Buttons (Hidden by default) -->
<div class="test-buttons-container" id="testButtonsContainer" style="display: none;">
<button id="testLevelBtn" class="test-level-button">
🧪 Test Scene (Debug)
</button>
<button id="viewReplaysBtn" class="test-level-button">
📹 View Replays
</button>
<br>
<a href="#/editor" class="level-create-link">
+ Create New Level
</a>
</div>
</div>
</div>
</div>
<!-- Editor View -->
<div data-view="editor" style="display: none;">
<div class="editor-container">
<a href="#/" class="back-link">← Back to Game</a>
<h1>🚀 Level Editor</h1>
<p class="subtitle">Configure and generate custom level configurations</p>
<div class="section">
<h2>Difficulty Presets</h2>
<div class="preset-buttons">
<button class="preset-btn" data-difficulty="recruit">Recruit</button>
<button class="preset-btn" data-difficulty="pilot">Pilot</button>
<button class="preset-btn" data-difficulty="captain">Captain</button>
<button class="preset-btn" data-difficulty="commander">Commander</button>
<button class="preset-btn" data-difficulty="test">Test</button>
<button class="preset-btn" data-difficulty="custom">Custom</button>
</div>
</div>
<div class="editor-grid">
<!-- Basic Settings -->
<div class="section">
<h2>⚙️ Basic Settings</h2>
<div class="form-group">
<label for="levelName">Level Name</label>
<input type="text" id="levelName" placeholder="my-custom-level">
</div>
<div class="form-group">
<label for="difficulty">Difficulty</label>
<select id="difficulty">
<option value="recruit">Recruit</option>
<option value="pilot">Pilot</option>
<option value="captain">Captain</option>
<option value="commander">Commander</option>
<option value="test">Test</option>
<option value="custom">Custom</option>
</select>
</div>
<div class="form-group">
<label for="author">Author (Optional)</label>
<input type="text" id="author" placeholder="Your name">
</div>
<div class="form-group">
<label for="description">Description (Optional)</label>
<input type="text" id="description" placeholder="Level description">
</div>
</div>
<!-- Ship Configuration -->
<div class="section">
<h2>🚀 Ship</h2>
<div class="form-group">
<label>Position</label>
<div class="vector-input">
<div>
<div class="vector-label">X</div>
<input type="number" id="shipX" value="0" step="0.1">
</div>
<div>
<div class="vector-label">Y</div>
<input type="number" id="shipY" value="1" step="0.1">
</div>
<div>
<div class="vector-label">Z</div>
<input type="number" id="shipZ" value="0" step="0.1">
</div>
</div>
</div>
</div>
<!-- Start Base Configuration -->
<div class="section">
<h2>🎯 Start Base</h2>
<div class="form-group">
<label>Position</label>
<div class="vector-input">
<div>
<div class="vector-label">X</div>
<input type="number" id="baseX" value="0" step="0.1">
</div>
<div>
<div class="vector-label">Y</div>
<input type="number" id="baseY" value="0" step="0.1">
</div>
<div>
<div class="vector-label">Z</div>
<input type="number" id="baseZ" value="0" step="0.1">
</div>
</div>
</div>
<div class="form-group">
<label>Base GLB Path</label>
<input type="text" id="baseGlbPath" value="base.glb" placeholder="base.glb">
</div>
</div>
<!-- Sun Configuration -->
<div class="section">
<h2>☀️ Sun</h2>
<div class="form-group">
<label>Position</label>
<div class="vector-input">
<div>
<div class="vector-label">X</div>
<input type="number" id="sunX" value="0" step="1">
</div>
<div>
<div class="vector-label">Y</div>
<input type="number" id="sunY" value="0" step="1">
</div>
<div>
<div class="vector-label">Z</div>
<input type="number" id="sunZ" value="400" step="1">
</div>
</div>
</div>
<div class="form-group">
<label for="sunDiameter">Diameter</label>
<input type="number" id="sunDiameter" value="50" step="1" min="1">
</div>
</div>
<!-- Planet Generation -->
<div class="section">
<h2>🪐 Planets</h2>
<div class="form-group">
<label for="planetCount">Count</label>
<input type="number" id="planetCount" value="12" min="0" max="50">
</div>
<div class="form-group">
<label for="planetMinDiam">Min Diameter</label>
<input type="number" id="planetMinDiam" value="100" step="10" min="10">
</div>
<div class="form-group">
<label for="planetMaxDiam">Max Diameter</label>
<input type="number" id="planetMaxDiam" value="200" step="10" min="10">
</div>
<div class="form-group">
<label for="planetMinDist">Min Distance from Sun</label>
<input type="number" id="planetMinDist" value="1000" step="100" min="100">
</div>
<div class="form-group">
<label for="planetMaxDist">Max Distance from Sun</label>
<input type="number" id="planetMaxDist" value="2000" step="100" min="100">
</div>
</div>
<!-- Asteroid Generation -->
<div class="section">
<h2>☄️ Asteroids</h2>
<div class="form-group">
<label for="asteroidCount">Count</label>
<input type="number" id="asteroidCount" value="20" min="1" max="200">
</div>
<div class="form-group">
<label for="forceMultiplier">Force Multiplier</label>
<input type="number" id="forceMultiplier" value="1.2" step="0.1" min="0.1">
<div class="help-text">Controls asteroid speed</div>
</div>
<div class="form-group">
<label for="asteroidMinSize">Min Size</label>
<input type="number" id="asteroidMinSize" value="2" step="0.5" min="0.5">
</div>
<div class="form-group">
<label for="asteroidMaxSize">Max Size</label>
<input type="number" id="asteroidMaxSize" value="7" step="0.5" min="0.5">
</div>
<div class="form-group">
<label for="asteroidMinDist">Min Distance</label>
<input type="number" id="asteroidMinDist" value="100" step="10" min="10">
<div class="help-text">Distance from start base</div>
</div>
<div class="form-group">
<label for="asteroidMaxDist">Max Distance</label>
<input type="number" id="asteroidMaxDist" value="250" step="10" min="10">
</div>
</div>
</div>
<div class="button-group">
<button class="btn-primary" id="generateBtn">Generate & Save</button>
<button class="btn-success" id="downloadBtn">Download JSON</button>
<button class="btn-secondary" id="copyBtn">Copy to Clipboard</button>
</div>
<div class="output-section" id="savedLevelsSection">
<h2>💾 Saved Levels</h2>
<div id="savedLevelsList"></div>
</div>
<div class="output-section editor-json-output" id="outputSection" style="display: none;">
<h2>Generated JSON</h2>
<p class="editor-json-note">
You can edit this JSON directly and save your changes.
</p>
<textarea id="jsonEditor" class="json-editor-textarea"></textarea>
<div class="editor-json-buttons">
<button class="btn-primary" id="saveEditedJsonBtn">Save Edited JSON</button>
<button class="btn-secondary" id="validateJsonBtn">Validate JSON</button>
</div>
<div id="jsonValidationMessage" class="json-validation-message"></div>
</div>
</div>
</div>
<!-- Controller Mapping View -->
<div data-view="controls" style="display: none;">
<div class="editor-container">
<a href="#/" class="back-link">← Back to Game</a>
<h1>🎮 Controller Mapping</h1>
<p class="subtitle">Customize VR controller button and stick mappings</p>
<div class="settings-grid">
<!-- Left Stick Section -->
<div class="section">
<h2>🕹️ Left Stick</h2>
<p class="settings-description">
Configure what actions the left thumbstick controls.
</p>
<div class="form-group">
<label for="leftStickX">Left Stick X-Axis (Left/Right)</label>
<select id="leftStickX" class="settings-select"></select>
<label class="checkbox-label" style="margin-top: 8px;">
<input type="checkbox" id="invertLeftStickX" class="settings-checkbox">
<span>Invert this axis</span>
</label>
</div>
<div class="form-group">
<label for="leftStickY">Left Stick Y-Axis (Up/Down)</label>
<select id="leftStickY" class="settings-select"></select>
<label class="checkbox-label" style="margin-top: 8px;">
<input type="checkbox" id="invertLeftStickY" class="settings-checkbox">
<span>Invert this axis</span>
</label>
</div>
</div>
<!-- Right Stick Section -->
<div class="section">
<h2>🕹️ Right Stick</h2>
<p class="settings-description">
Configure what actions the right thumbstick controls.
</p>
<div class="form-group">
<label for="rightStickX">Right Stick X-Axis (Left/Right)</label>
<select id="rightStickX" class="settings-select"></select>
<label class="checkbox-label" style="margin-top: 8px;">
<input type="checkbox" id="invertRightStickX" class="settings-checkbox">
<span>Invert this axis</span>
</label>
</div>
<div class="form-group">
<label for="rightStickY">Right Stick Y-Axis (Up/Down)</label>
<select id="rightStickY" class="settings-select"></select>
<label class="checkbox-label" style="margin-top: 8px;">
<input type="checkbox" id="invertRightStickY" class="settings-checkbox">
<span>Invert this axis</span>
</label>
</div>
</div>
<!-- Button Mappings Section -->
<div class="section">
<h2>🔘 Button Mappings</h2>
<p class="settings-description">
Configure what actions each controller button performs.
</p>
<div class="form-group">
<label for="trigger">Trigger (Index Finger)</label>
<select id="trigger" class="settings-select"></select>
</div>
<div class="form-group">
<label for="aButton">A Button (Right Controller)</label>
<select id="aButton" class="settings-select"></select>
</div>
<div class="form-group">
<label for="bButton">B Button (Right Controller)</label>
<select id="bButton" class="settings-select"></select>
</div>
<div class="form-group">
<label for="xButton">X Button (Left Controller)</label>
<select id="xButton" class="settings-select"></select>
</div>
<div class="form-group">
<label for="yButton">Y Button (Left Controller)</label>
<select id="yButton" class="settings-select"></select>
</div>
<div class="form-group">
<label for="squeeze">Squeeze/Grip Button</label>
<select id="squeeze" class="settings-select"></select>
</div>
</div>
<!-- Info Section -->
<div class="section">
<h2> Action Guide</h2>
<div class="settings-info-content">
<p><strong class="settings-label">Yaw:</strong> Turn left/right (rotate around vertical axis)</p>
<p><strong class="settings-label">Pitch:</strong> Nose up/down (rotate around horizontal axis)</p>
<p><strong class="settings-label">Roll:</strong> Barrel roll (rotate around forward axis)</p>
<p><strong class="settings-label">Forward:</strong> Forward and backward thrust</p>
<p><strong class="settings-label">None:</strong> No action assigned</p>
</div>
</div>
<!-- Storage Info -->
<div class="section">
<h2>💾 Storage Info</h2>
<div class="settings-info-content">
<p>Controller mappings are automatically saved to your browser's local storage and will persist between sessions.</p>
<p class="settings-warning">
⚠️ Note: Changes will take effect when you start a new level. Restart the current level to see changes.
</p>
</div>
</div>
</div>
<div class="button-group">
<button class="btn-primary" id="saveControlsBtn">💾 Save Mapping</button>
<button class="btn-secondary" id="resetControlsBtn">🔄 Reset to Default</button>
<button class="btn-secondary" id="testControlsBtn">👁️ Preview Mapping</button>
</div>
<div id="controlsMessage" class="settings-message"></div>
</div>
</div>
<!-- Settings View -->
<div data-view="settings" style="display: none;">
<div class="editor-container">
<a href="#/" class="back-link">← Back to Game</a>
<h1>⚙️ Game Settings</h1>
<p class="subtitle">Configure graphics quality and physics settings</p>
<div class="settings-grid">
<!-- Physics Settings -->
<div class="section">
<h2>⚛️ Physics</h2>
<p class="settings-description">
Disabling physics can significantly improve performance but will prevent gameplay.
</p>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="physicsEnabled" class="settings-checkbox">
<span>Enable Physics</span>
</label>
<div class="help-text">
Required for collisions, shooting, and asteroid movement. Disabling this will prevent gameplay but may help with debugging or viewing the scene.
</div>
</div>
</div>
<!-- Debug Settings -->
<div class="section">
<h2>🐛 Developer</h2>
<p class="settings-description">
Enable debug logging to console for troubleshooting and development.
</p>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="debugEnabled" class="settings-checkbox">
<span>Enable Debug Logging</span>
</label>
<div class="help-text">
When enabled, debug messages will be shown in the browser console. Useful for development and troubleshooting issues.
</div>
</div>
</div>
<!-- Ship Physics Settings -->
<div class="section">
<h2>🚀 Ship Physics</h2>
<p class="settings-description">
Advanced tuning parameters for ship movement and handling. Adjust these to customize how the ship responds to controls.
</p>
<div class="form-group">
<label for="maxLinearVelocity">Max Linear Velocity</label>
<input type="number" id="maxLinearVelocity" value="200" step="10" min="50" max="1000">
<div class="help-text">
Maximum forward/backward speed of the ship. Higher values allow faster movement.
</div>
</div>
<div class="form-group">
<label for="maxAngularVelocity">Max Angular Velocity</label>
<input type="number" id="maxAngularVelocity" value="1.4" step="0.1" min="0.5" max="5.0">
<div class="help-text">
Maximum rotation speed of the ship. Higher values allow faster turning.
</div>
</div>
<div class="form-group">
<label for="linearForceMultiplier">Linear Force Multiplier</label>
<input type="number" id="linearForceMultiplier" value="800" step="50" min="100" max="3000">
<div class="help-text">
Acceleration power for forward/backward thrust. Higher values = faster acceleration.
</div>
</div>
<div class="form-group">
<label for="angularForceMultiplier">Angular Force Multiplier</label>
<input type="number" id="angularForceMultiplier" value="15" step="1" min="5" max="50">
<div class="help-text">
Torque power for rotation. Higher values = faster rotational acceleration.
</div>
</div>
</div>
<!-- Info Section -->
<div class="section">
<h2> Quality Level Guide</h2>
<div class="settings-info-content">
<p><strong class="settings-label">Wireframe:</strong> Minimal rendering, shows mesh structure only. Best for debugging or very low-end devices.</p>
<p><strong class="settings-label">Simple Material:</strong> Basic solid colors without textures. Good performance with basic visuals.</p>
<p><strong class="settings-label">Full Texture:</strong> Standard textures with procedural generation. Recommended for most users.</p>
<p><strong class="settings-label">PBR Texture:</strong> Physically-based rendering with enhanced materials. Best visual quality but higher GPU usage.</p>
</div>
</div>
<!-- Current Config Display -->
<div class="section">
<h2>💾 Storage Info</h2>
<div class="settings-info-content">
<p>Settings are automatically saved to your browser's local storage and will persist between sessions.</p>
<p class="settings-warning">
⚠️ Note: Changes will take effect when you start a new level. Restart the current level to see changes.
</p>
</div>
</div>
</div>
<div class="button-group">
<button class="btn-primary" id="saveSettingsBtn">💾 Save Settings</button>
<button class="btn-secondary" id="resetSettingsBtn">🔄 Reset to Defaults</button>
</div>
<div id="settingsMessage" class="settings-message"></div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>