apirrone commited on
Commit
626e568
·
0 Parent(s):

Initial commit

Browse files
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ __pycache__/
2
+ *.egg-info/
3
+ build/
README.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Spaceship Game
3
+ emoji: 👋
4
+ colorFrom: red
5
+ colorTo: blue
6
+ sdk: static
7
+ pinned: false
8
+ short_description: Write your description here
9
+ tags:
10
+ - reachy_mini
11
+ - reachy_mini_python_app
12
+ ---
index.html ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width" />
7
+ <title> Spaceship Game </title>
8
+ <link rel="stylesheet" href="style.css" />
9
+ </head>
10
+
11
+ <body>
12
+ <div class="hero">
13
+ <div class="hero-content">
14
+ <div class="app-icon">🤖⚡</div>
15
+ <h1> Spaceship Game </h1>
16
+ <p class="tagline">Enter your tagline here</p>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="container">
21
+ <div class="main-card">
22
+ <div class="app-preview">
23
+ <div class="preview-image">
24
+ <div class="camera-feed">🛠️</div>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </div>
29
+
30
+ <div class="footer">
31
+ <p>
32
+ 🤖 Spaceship Game •
33
+ <a href="https://github.com/pollen-robotics" target="_blank">Pollen Robotics</a> •
34
+ <a href="https://huggingface.co/spaces/pollen-robotics/reachy-mini-landing-page#apps" target="_blank">Browse More
35
+ Apps</a>
36
+ </p>
37
+ </div>
38
+ </body>
39
+
40
+ </html>
pyproject.toml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+
6
+ [project]
7
+ name = "spaceship_game"
8
+ version = "0.1.0"
9
+ description = "Add your description here"
10
+ readme = "README.md"
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "reachy-mini"
14
+ ]
15
+ keywords = ["reachy-mini-app"]
16
+
17
+ [project.entry-points."reachy_mini_apps"]
18
+ spaceship_game = "spaceship_game.main:SpaceshipGame"
19
+
20
+ [tool.setuptools]
21
+ package-dir = { "" = "." }
22
+ include-package-data = true
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["."]
26
+
27
+ [tool.setuptools.package-data]
28
+ spaceship_game = ["**/*"] # Also include all non-.py files
spaceship_game/__init__.py ADDED
File without changes
spaceship_game/main.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import threading
2
+ import logging
3
+ from reachy_mini import ReachyMini, ReachyMiniApp
4
+ import numpy as np
5
+ import time
6
+ from scipy.spatial.transform import Rotation as R
7
+ from pydantic import BaseModel
8
+
9
+ # Disable uvicorn access logs to prevent spam
10
+ logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
11
+
12
+
13
+ class ScoreData(BaseModel):
14
+ points: int
15
+
16
+
17
+ class DamageData(BaseModel):
18
+ damage: int
19
+
20
+
21
+ class SpaceshipGame(ReachyMiniApp):
22
+ custom_app_url: str | None = "http://0.0.0.0:8042"
23
+ request_media_backend: str | None = None
24
+
25
+ def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
26
+ # Shared state for sensor data
27
+ current_head_pose = np.eye(4)
28
+ current_antennas = [0.0, 0.0]
29
+ game_score = 0
30
+ player_health = 100
31
+
32
+ # Game state
33
+ fire_left = False
34
+ fire_right = False
35
+
36
+ # Add endpoints to serve sensor data to the frontend
37
+ @self.settings_app.get("/sensor_data")
38
+ def get_sensor_data():
39
+ # Extract rotation from head pose
40
+ rotation_matrix = current_head_pose[:3, :3]
41
+ r = R.from_matrix(rotation_matrix)
42
+ roll, pitch, yaw = r.as_euler('xyz', degrees=True)
43
+
44
+ return {
45
+ "roll": float(roll),
46
+ "pitch": float(pitch),
47
+ "yaw": float(yaw),
48
+ "antennas": {
49
+ "right": float(current_antennas[0]),
50
+ "left": float(current_antennas[1])
51
+ },
52
+ "fire_left": fire_left,
53
+ "fire_right": fire_right,
54
+ "score": game_score,
55
+ "health": player_health
56
+ }
57
+
58
+ @self.settings_app.post("/add_score")
59
+ def add_score_endpoint(data: ScoreData):
60
+ nonlocal game_score
61
+ game_score += data.points
62
+ return {"score": game_score}
63
+
64
+ @self.settings_app.post("/damage_player")
65
+ def damage_player_endpoint(data: DamageData):
66
+ nonlocal player_health
67
+ player_health = max(0, player_health - data.damage)
68
+ return {"health": player_health}
69
+
70
+ @self.settings_app.post("/reset_game")
71
+ def reset_game():
72
+ nonlocal game_score, player_health
73
+ game_score = 0
74
+ player_health = 100
75
+ return {"score": game_score, "health": player_health}
76
+
77
+ # Main control loop - read sensors from Reachy Mini
78
+ while not stop_event.is_set():
79
+ # Read current head pose from the robot
80
+ current_head_pose = reachy_mini.get_current_head_pose()
81
+
82
+ # Read current antenna positions
83
+ current_antennas = reachy_mini.get_present_antenna_joint_positions()
84
+
85
+ # Detect antenna triggers - pull down to shoot
86
+ # Antenna [0] is right antenna: pull down = negative value < -threshold
87
+ # Antenna [1] is left antenna: pull down = positive value > threshold
88
+ # Lower threshold = more sensitive (0.25 instead of 0.5 = 2x more sensitive)
89
+ PULL_THRESHOLD = 0.25
90
+
91
+ # Right antenna controls LEFT gun
92
+ if current_antennas[0] < -PULL_THRESHOLD:
93
+ if not fire_left:
94
+ fire_left = True
95
+ else:
96
+ fire_left = False
97
+
98
+ # Left antenna controls RIGHT gun
99
+ if current_antennas[1] > PULL_THRESHOLD:
100
+ if not fire_right:
101
+ fire_right = True
102
+ else:
103
+ fire_right = False
104
+
105
+ time.sleep(0.02)
106
+
107
+
108
+ if __name__ == "__main__":
109
+ app = SpaceshipGame()
110
+ try:
111
+ app.wrapped_run()
112
+ except KeyboardInterrupt:
113
+ app.stop()
spaceship_game/static/index.html ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <title>Spaceship Game - Reachy Mini</title>
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <style>
9
+ body {
10
+ margin: 0;
11
+ overflow: hidden;
12
+ font-family: 'Arial', sans-serif;
13
+ background: #000;
14
+ }
15
+
16
+ #game-canvas {
17
+ display: block;
18
+ width: 100vw;
19
+ height: 100vh;
20
+ }
21
+
22
+ #hud {
23
+ position: fixed;
24
+ top: 20px;
25
+ left: 20px;
26
+ color: #0ff;
27
+ font-size: 24px;
28
+ text-shadow: 0 0 10px #0ff;
29
+ z-index: 100;
30
+ font-family: 'Courier New', monospace;
31
+ }
32
+
33
+ #debug-info {
34
+ position: fixed;
35
+ bottom: 20px;
36
+ left: 20px;
37
+ color: #0f0;
38
+ font-size: 14px;
39
+ text-shadow: 0 0 5px #0f0;
40
+ z-index: 100;
41
+ font-family: 'Courier New', monospace;
42
+ background: rgba(0, 0, 0, 0.5);
43
+ padding: 10px;
44
+ border-radius: 5px;
45
+ }
46
+
47
+ #instructions {
48
+ position: fixed;
49
+ top: 20px;
50
+ right: 20px;
51
+ color: #fff;
52
+ font-size: 16px;
53
+ z-index: 100;
54
+ background: rgba(0, 0, 0, 0.7);
55
+ padding: 15px;
56
+ border-radius: 10px;
57
+ max-width: 300px;
58
+ }
59
+
60
+ .crosshair {
61
+ position: fixed;
62
+ top: 50%;
63
+ left: 50%;
64
+ transform: translate(-50%, -50%);
65
+ width: 40px;
66
+ height: 40px;
67
+ border: 2px solid #0ff;
68
+ border-radius: 50%;
69
+ pointer-events: none;
70
+ z-index: 50;
71
+ opacity: 0.7;
72
+ transition: all 0.05s ease-out;
73
+ box-shadow: 0 0 10px #0ff, inset 0 0 10px #0ff;
74
+ }
75
+
76
+ .crosshair::before,
77
+ .crosshair::after {
78
+ content: '';
79
+ position: absolute;
80
+ background: #0ff;
81
+ }
82
+
83
+ .crosshair::before {
84
+ width: 2px;
85
+ height: 20px;
86
+ left: 50%;
87
+ top: -10px;
88
+ transform: translateX(-50%);
89
+ }
90
+
91
+ .crosshair::after {
92
+ width: 20px;
93
+ height: 2px;
94
+ top: 50%;
95
+ left: -10px;
96
+ transform: translateY(-50%);
97
+ }
98
+ </style>
99
+ </head>
100
+
101
+ <body>
102
+ <div id="game-canvas"></div>
103
+ <div class="crosshair"></div>
104
+
105
+ <div id="hud">
106
+ <div>SCORE: <span id="score">0</span></div>
107
+ <div id="status">HP: <span style="color: #0f0">100</span></div>
108
+ </div>
109
+
110
+ <div id="debug-info">
111
+ <div>Roll: <span id="roll">0</span>°</div>
112
+ <div>Pitch: <span id="pitch">0</span>°</div>
113
+ <div>Yaw: <span id="yaw">0</span>°</div>
114
+ <div>Antennas: <span id="antennas">0, 0</span></div>
115
+ </div>
116
+
117
+ <div id="instructions">
118
+ <h3>SHMUP Controls</h3>
119
+ <p><strong>Defend yourself from enemy ships!</strong></p>
120
+ <ul>
121
+ <li><strong>Tilt up/down</strong> → Aim up/down</li>
122
+ <li><strong>Turn left/right</strong> → Aim left/right</li>
123
+ <li><strong>Tilt left/right</strong> → Banking (visual)</li>
124
+ <li><strong>Pull antennas</strong> → Fire guns!</li>
125
+ </ul>
126
+ <hr style="border-color: #444;">
127
+ <p><strong>Enemy Types:</strong></p>
128
+ <ul style="font-size: 14px;">
129
+ <li>🔴 Red: Basic (100pts)</li>
130
+ <li>🟠 Orange: Fast &amp; Aimed (150pts)</li>
131
+ <li>🟣 Purple: Heavy Spread (300pts)</li>
132
+ </ul>
133
+ </div>
134
+
135
+ <script type="importmap">
136
+ {
137
+ "imports": {
138
+ "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
139
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
140
+ }
141
+ }
142
+ </script>
143
+ <script type="module" src="/static/main.js"></script>
144
+ </body>
145
+
146
+ </html>
spaceship_game/static/main.js ADDED
@@ -0,0 +1,847 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as THREE from 'three';
2
+
3
+ // Game state
4
+ let scene, camera, renderer;
5
+ let spaceship, spaceshipParts = {};
6
+ let stars = [];
7
+ let asteroids = [];
8
+ let enemies = [];
9
+ let projectiles = [];
10
+ let enemyBullets = [];
11
+ let explosions = [];
12
+ let sensorData = { roll: 0, pitch: 0, yaw: 0, antennas: { right: 0, left: 0 }, fire_left: false, fire_right: false, score: 0, health: 100 };
13
+ let lastFireLeftState = false;
14
+ let lastFireRightState = false;
15
+
16
+ // Game settings
17
+ const SMOOTHING = 0.15;
18
+ const MOVEMENT_SCALE = 1.8;
19
+ const YAW_MOVEMENT_SCALE = 0.3;
20
+ const SPEED = 0.5;
21
+ const FORWARD_SPEED = 1.5; // Speed moving through space
22
+ const MAX_ASTEROIDS = 10;
23
+ const ASTEROID_SPAWN_INTERVAL = 3000;
24
+ const ENEMY_SPAWN_INTERVAL = 2500;
25
+ const MAX_ENEMIES = 10;
26
+
27
+ // Initialize the scene
28
+ function init() {
29
+ // Scene setup
30
+ scene = new THREE.Scene();
31
+ scene.fog = new THREE.FogExp2(0x000000, 0.0008);
32
+
33
+ // Camera setup - positioned behind the spaceship
34
+ camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
35
+ camera.position.set(0, 2, 8);
36
+ camera.lookAt(0, 0, 0);
37
+
38
+ // Renderer setup
39
+ renderer = new THREE.WebGLRenderer({ antialias: true });
40
+ renderer.setSize(window.innerWidth, window.innerHeight);
41
+ renderer.setPixelRatio(window.devicePixelRatio);
42
+ document.getElementById('game-canvas').appendChild(renderer.domElement);
43
+
44
+ // Lighting
45
+ const ambientLight = new THREE.AmbientLight(0x404040, 1);
46
+ scene.add(ambientLight);
47
+
48
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
49
+ directionalLight.position.set(5, 10, 7.5);
50
+ scene.add(directionalLight);
51
+
52
+ // Create starfield
53
+ createStarfield();
54
+
55
+ // Create spaceship
56
+ createSpaceship();
57
+
58
+ // Create initial asteroids
59
+ for (let i = 0; i < 5; i++) {
60
+ createAsteroid();
61
+ }
62
+
63
+ // Handle window resize
64
+ window.addEventListener('resize', onWindowResize, false);
65
+
66
+ // Start game loop
67
+ animate();
68
+
69
+ // Start fetching sensor data
70
+ fetchSensorData();
71
+
72
+ // Spawn asteroids and enemies periodically
73
+ setInterval(createAsteroid, ASTEROID_SPAWN_INTERVAL);
74
+ setInterval(createEnemy, ENEMY_SPAWN_INTERVAL);
75
+ }
76
+
77
+ function createSpaceship() {
78
+ spaceship = new THREE.Group();
79
+
80
+ // Main body - fuselage
81
+ const bodyGeometry = new THREE.ConeGeometry(0.5, 2, 8);
82
+ const bodyMaterial = new THREE.MeshPhongMaterial({
83
+ color: 0x00ccff,
84
+ emissive: 0x004466,
85
+ shininess: 100
86
+ });
87
+ const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
88
+ body.rotation.x = Math.PI / 2;
89
+ spaceship.add(body);
90
+ spaceshipParts.body = body;
91
+
92
+ // Wings
93
+ const wingGeometry = new THREE.BoxGeometry(3, 0.1, 0.8);
94
+ const wingMaterial = new THREE.MeshPhongMaterial({
95
+ color: 0x0099cc,
96
+ emissive: 0x003344
97
+ });
98
+ const wings = new THREE.Mesh(wingGeometry, wingMaterial);
99
+ wings.position.z = 0.3;
100
+ spaceship.add(wings);
101
+ spaceshipParts.wings = wings;
102
+
103
+ // Cockpit
104
+ const cockpitGeometry = new THREE.SphereGeometry(0.3, 16, 16);
105
+ const cockpitMaterial = new THREE.MeshPhongMaterial({
106
+ color: 0x88ffff,
107
+ emissive: 0x004466,
108
+ transparent: true,
109
+ opacity: 0.8
110
+ });
111
+ const cockpit = new THREE.Mesh(cockpitGeometry, cockpitMaterial);
112
+ cockpit.position.z = -0.5;
113
+ cockpit.scale.z = 0.7;
114
+ spaceship.add(cockpit);
115
+ spaceshipParts.cockpit = cockpit;
116
+
117
+ // Engine glow
118
+ const engineGeometry = new THREE.CylinderGeometry(0.2, 0.3, 0.5, 8);
119
+ const engineMaterial = new THREE.MeshBasicMaterial({
120
+ color: 0xff6600,
121
+ transparent: true,
122
+ opacity: 0.8
123
+ });
124
+ const engineGlow = new THREE.Mesh(engineGeometry, engineMaterial);
125
+ engineGlow.rotation.x = Math.PI / 2;
126
+ engineGlow.position.z = 1.2;
127
+ spaceship.add(engineGlow);
128
+ spaceshipParts.engine = engineGlow;
129
+
130
+ // Add point light for engine
131
+ const engineLight = new THREE.PointLight(0xff6600, 2, 10);
132
+ engineLight.position.z = 1.5;
133
+ spaceship.add(engineLight);
134
+ spaceshipParts.engineLight = engineLight;
135
+
136
+ scene.add(spaceship);
137
+ }
138
+
139
+ function createStarfield() {
140
+ const starGeometry = new THREE.BufferGeometry();
141
+ const starPositions = [];
142
+
143
+ for (let i = 0; i < 2000; i++) {
144
+ const x = (Math.random() - 0.5) * 2000;
145
+ const y = (Math.random() - 0.5) * 2000;
146
+ const z = Math.random() * -2000; // Stars in front of us
147
+ starPositions.push(x, y, z);
148
+ }
149
+
150
+ starGeometry.setAttribute('position', new THREE.Float32BufferAttribute(starPositions, 3));
151
+
152
+ const starMaterial = new THREE.PointsMaterial({
153
+ color: 0xffffff,
154
+ size: 2,
155
+ transparent: true
156
+ });
157
+
158
+ const starField = new THREE.Points(starGeometry, starMaterial);
159
+ scene.add(starField);
160
+ stars.push(starField);
161
+ }
162
+
163
+ function createAsteroid() {
164
+ if (asteroids.length >= MAX_ASTEROIDS) return;
165
+
166
+ const size = Math.random() * 2 + 1;
167
+ const geometry = new THREE.DodecahedronGeometry(size, 0);
168
+ const material = new THREE.MeshPhongMaterial({
169
+ color: 0x888888,
170
+ flatShading: true
171
+ });
172
+ const asteroid = new THREE.Mesh(geometry, material);
173
+
174
+ // Random position in front of spaceship
175
+ asteroid.position.x = (Math.random() - 0.5) * 50;
176
+ asteroid.position.y = (Math.random() - 0.5) * 50;
177
+ asteroid.position.z = -100 - Math.random() * 50;
178
+
179
+ // Random rotation
180
+ asteroid.rotation.x = Math.random() * Math.PI;
181
+ asteroid.rotation.y = Math.random() * Math.PI;
182
+
183
+ // Random velocity (z is relative to forward speed)
184
+ asteroid.userData.velocity = {
185
+ x: (Math.random() - 0.5) * 0.1,
186
+ y: (Math.random() - 0.5) * 0.1,
187
+ z: Math.random() * 0.5
188
+ };
189
+
190
+ asteroid.userData.rotationSpeed = {
191
+ x: (Math.random() - 0.5) * 0.02,
192
+ y: (Math.random() - 0.5) * 0.02,
193
+ z: (Math.random() - 0.5) * 0.02
194
+ };
195
+
196
+ scene.add(asteroid);
197
+ asteroids.push(asteroid);
198
+ }
199
+
200
+ function createEnemy() {
201
+ if (enemies.length >= MAX_ENEMIES) return;
202
+
203
+ const enemyGroup = new THREE.Group();
204
+
205
+ // Enemy type (0: basic, 1: fast, 2: heavy)
206
+ const type = Math.floor(Math.random() * 3);
207
+
208
+ let color, size, health, points, shootPattern;
209
+
210
+ switch(type) {
211
+ case 0: // Basic enemy
212
+ color = 0xff3333;
213
+ size = 1;
214
+ health = 2;
215
+ points = 100;
216
+ shootPattern = 'straight';
217
+ break;
218
+ case 1: // Fast enemy
219
+ color = 0xffaa00;
220
+ size = 0.7;
221
+ health = 1;
222
+ points = 150;
223
+ shootPattern = 'aimed';
224
+ break;
225
+ case 2: // Heavy enemy
226
+ color = 0xff00ff;
227
+ size = 1.5;
228
+ health = 5;
229
+ points = 300;
230
+ shootPattern = 'spread';
231
+ break;
232
+ }
233
+
234
+ // Create enemy ship model
235
+ // Main body - inverted cone (pointy end forward)
236
+ const bodyGeometry = new THREE.ConeGeometry(size * 0.5, size * 1.5, 6);
237
+ const bodyMaterial = new THREE.MeshPhongMaterial({
238
+ color: color,
239
+ emissive: color,
240
+ emissiveIntensity: 0.3,
241
+ flatShading: true
242
+ });
243
+ const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
244
+ body.rotation.x = -Math.PI / 2; // Point forward (toward player)
245
+ enemyGroup.add(body);
246
+
247
+ // Wings
248
+ const wingGeometry = new THREE.BoxGeometry(size * 2.5, size * 0.1, size * 0.6);
249
+ const wingMaterial = new THREE.MeshPhongMaterial({
250
+ color: color,
251
+ emissive: color,
252
+ emissiveIntensity: 0.2
253
+ });
254
+ const wings = new THREE.Mesh(wingGeometry, wingMaterial);
255
+ wings.position.z = size * 0.3;
256
+ enemyGroup.add(wings);
257
+
258
+ // Cockpit/core
259
+ const cockpitGeometry = new THREE.SphereGeometry(size * 0.3, 8, 8);
260
+ const cockpitMaterial = new THREE.MeshPhongMaterial({
261
+ color: 0xffffff,
262
+ emissive: color,
263
+ emissiveIntensity: 0.8
264
+ });
265
+ const cockpit = new THREE.Mesh(cockpitGeometry, cockpitMaterial);
266
+ cockpit.position.z = -size * 0.3;
267
+ enemyGroup.add(cockpit);
268
+
269
+ // Engine glow at back
270
+ const engineGeometry = new THREE.CylinderGeometry(size * 0.15, size * 0.25, size * 0.4, 6);
271
+ const engineMaterial = new THREE.MeshBasicMaterial({
272
+ color: 0xff0000,
273
+ transparent: true,
274
+ opacity: 0.8
275
+ });
276
+ const engine = new THREE.Mesh(engineGeometry, engineMaterial);
277
+ engine.rotation.x = Math.PI / 2;
278
+ engine.position.z = size * 0.8;
279
+ enemyGroup.add(engine);
280
+
281
+ // Outer glow
282
+ const glowGeometry = new THREE.SphereGeometry(size * 1.5, 8, 8);
283
+ const glowMaterial = new THREE.MeshBasicMaterial({
284
+ color: color,
285
+ transparent: true,
286
+ opacity: 0.1
287
+ });
288
+ const glow = new THREE.Mesh(glowGeometry, glowMaterial);
289
+ enemyGroup.add(glow);
290
+
291
+ // Position enemy far away, will move into screen
292
+ enemyGroup.position.x = (Math.random() - 0.5) * 50;
293
+ enemyGroup.position.y = (Math.random() - 0.5) * 40;
294
+ enemyGroup.position.z = -120 - Math.random() * 20;
295
+
296
+ // Movement pattern (0: horizontal sweep, 1: sine wave, 2: circle, 3: stationary)
297
+ const movementPattern = Math.floor(Math.random() * 4);
298
+
299
+ // Set up movement parameters
300
+ let moveSpeed, targetZ, moveDirection;
301
+ switch(movementPattern) {
302
+ case 0: // Horizontal sweep
303
+ moveSpeed = 0.3;
304
+ targetZ = -40; // Stop closer to player
305
+ moveDirection = Math.random() > 0.5 ? 1 : -1;
306
+ break;
307
+ case 1: // Sine wave
308
+ moveSpeed = 0.2;
309
+ targetZ = -35;
310
+ moveDirection = Math.random() > 0.5 ? 1 : -1;
311
+ break;
312
+ case 2: // Circle
313
+ moveSpeed = 0.15;
314
+ targetZ = -30;
315
+ moveDirection = Math.random() > 0.5 ? 1 : -1;
316
+ break;
317
+ case 3: // Stationary sniper
318
+ moveSpeed = 0.4;
319
+ targetZ = -50;
320
+ moveDirection = 0;
321
+ break;
322
+ }
323
+
324
+ enemyGroup.userData = {
325
+ type: type,
326
+ health: health,
327
+ maxHealth: health,
328
+ points: points,
329
+ shootPattern: shootPattern,
330
+ movementPattern: movementPattern,
331
+ moveSpeed: moveSpeed,
332
+ targetZ: targetZ,
333
+ moveDirection: moveDirection,
334
+ hasReachedPosition: false,
335
+ lastShootTime: Date.now() + Math.random() * 2000,
336
+ shootCooldown: 1500 + Math.random() * 1000,
337
+ timeAlive: 0,
338
+ circleRadius: 10 + Math.random() * 5,
339
+ circleAngle: Math.random() * Math.PI * 2,
340
+ initialX: (Math.random() - 0.5) * 50,
341
+ initialY: (Math.random() - 0.5) * 40,
342
+ body: body,
343
+ glow: glow,
344
+ engine: engine,
345
+ cockpit: cockpit
346
+ };
347
+
348
+ scene.add(enemyGroup);
349
+ enemies.push(enemyGroup);
350
+ }
351
+
352
+ function createEnemyBullet(enemy, pattern) {
353
+ const bulletGeometry = new THREE.SphereGeometry(0.15, 8, 8);
354
+ const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
355
+
356
+ const playerPos = spaceship.position.clone();
357
+ const enemyPos = enemy.position.clone();
358
+
359
+ switch(pattern) {
360
+ case 'straight':
361
+ // Single bullet straight down
362
+ const bullet1 = new THREE.Mesh(bulletGeometry, bulletMaterial);
363
+ bullet1.position.copy(enemyPos);
364
+ bullet1.userData.velocity = new THREE.Vector3(0, 0, 2);
365
+ scene.add(bullet1);
366
+ enemyBullets.push(bullet1);
367
+ break;
368
+
369
+ case 'aimed':
370
+ // Aimed at player
371
+ const bullet2 = new THREE.Mesh(bulletGeometry, bulletMaterial);
372
+ bullet2.position.copy(enemyPos);
373
+ const direction = playerPos.sub(enemyPos).normalize();
374
+ bullet2.userData.velocity = direction.multiplyScalar(2.5);
375
+ scene.add(bullet2);
376
+ enemyBullets.push(bullet2);
377
+ break;
378
+
379
+ case 'spread':
380
+ // 5-way spread
381
+ for (let i = 0; i < 5; i++) {
382
+ const bullet3 = new THREE.Mesh(bulletGeometry, bulletMaterial.clone());
383
+ bullet3.position.copy(enemyPos);
384
+ const angle = (i - 2) * 0.3; // -0.6 to 0.6 radians
385
+ const vel = new THREE.Vector3(
386
+ Math.sin(angle) * 2,
387
+ 0,
388
+ Math.cos(angle) * 2
389
+ );
390
+ bullet3.userData.velocity = vel;
391
+ scene.add(bullet3);
392
+ enemyBullets.push(bullet3);
393
+ }
394
+ break;
395
+ }
396
+ }
397
+
398
+ function createExplosion(position, color = 0xff6600) {
399
+ const particleCount = 20;
400
+
401
+ for (let i = 0; i < particleCount; i++) {
402
+ const geometry = new THREE.SphereGeometry(0.2, 4, 4);
403
+ const material = new THREE.MeshBasicMaterial({ color: color });
404
+ const particle = new THREE.Mesh(geometry, material);
405
+
406
+ particle.position.copy(position);
407
+
408
+ const velocity = new THREE.Vector3(
409
+ (Math.random() - 0.5) * 3,
410
+ (Math.random() - 0.5) * 3,
411
+ (Math.random() - 0.5) * 3
412
+ );
413
+
414
+ particle.userData = {
415
+ velocity: velocity,
416
+ lifetime: 30,
417
+ decay: 0.05
418
+ };
419
+
420
+ scene.add(particle);
421
+ explosions.push(particle);
422
+ }
423
+ }
424
+
425
+ function createProjectile(side) {
426
+ const geometry = new THREE.SphereGeometry(0.2, 8, 8);
427
+ const color = side === 'left' ? 0xff00ff : 0x00ffff; // Pink for left, cyan for right
428
+ const material = new THREE.MeshBasicMaterial({ color: color });
429
+ const projectile = new THREE.Mesh(geometry, material);
430
+
431
+ // Position based on which gun is firing (in local space)
432
+ const offsetX = side === 'left' ? -1.2 : 1.2;
433
+ const gunPosition = new THREE.Vector3(offsetX, 0, -2);
434
+
435
+ // Transform gun position to world space based on spaceship rotation and position
436
+ gunPosition.applyEuler(spaceship.rotation);
437
+ gunPosition.add(spaceship.position);
438
+
439
+ projectile.position.copy(gunPosition);
440
+
441
+ // Add glow
442
+ const glowGeometry = new THREE.SphereGeometry(0.4, 8, 8);
443
+ const glowMaterial = new THREE.MeshBasicMaterial({
444
+ color: color,
445
+ transparent: true,
446
+ opacity: 0.3
447
+ });
448
+ const glow = new THREE.Mesh(glowGeometry, glowMaterial);
449
+ projectile.add(glow);
450
+
451
+ // Calculate velocity direction based on spaceship rotation
452
+ const direction = new THREE.Vector3(0, 0, -1); // Forward direction
453
+ direction.applyEuler(spaceship.rotation);
454
+ direction.normalize();
455
+
456
+ projectile.userData.velocity = direction.multiplyScalar(5);
457
+
458
+ scene.add(projectile);
459
+ projectiles.push(projectile);
460
+ }
461
+
462
+ async function fetchSensorData() {
463
+ try {
464
+ const response = await fetch('/sensor_data');
465
+ const data = await response.json();
466
+
467
+ // Smooth the sensor data
468
+ sensorData.roll = sensorData.roll * (1 - SMOOTHING) + data.roll * SMOOTHING;
469
+ sensorData.pitch = sensorData.pitch * (1 - SMOOTHING) + data.pitch * SMOOTHING;
470
+ sensorData.yaw = sensorData.yaw * (1 - SMOOTHING) + data.yaw * SMOOTHING;
471
+ sensorData.antennas = data.antennas;
472
+ sensorData.score = data.score;
473
+ sensorData.health = data.health;
474
+
475
+ // Detect left gun fire trigger (rising edge)
476
+ if (data.fire_left && !lastFireLeftState) {
477
+ createProjectile('left');
478
+ lastFireLeftState = true;
479
+ } else if (!data.fire_left) {
480
+ lastFireLeftState = false;
481
+ }
482
+
483
+ // Detect right gun fire trigger (rising edge)
484
+ if (data.fire_right && !lastFireRightState) {
485
+ createProjectile('right');
486
+ lastFireRightState = true;
487
+ } else if (!data.fire_right) {
488
+ lastFireRightState = false;
489
+ }
490
+
491
+ // Update HUD
492
+ updateHUD();
493
+
494
+ } catch (error) {
495
+ console.error('Error fetching sensor data:', error);
496
+ }
497
+
498
+ // Fetch at 50Hz
499
+ setTimeout(fetchSensorData, 20);
500
+ }
501
+
502
+ function updateHUD() {
503
+ document.getElementById('score').textContent = sensorData.score;
504
+ document.getElementById('roll').textContent = sensorData.roll.toFixed(1);
505
+ document.getElementById('pitch').textContent = sensorData.pitch.toFixed(1);
506
+ document.getElementById('yaw').textContent = sensorData.yaw.toFixed(1);
507
+ document.getElementById('antennas').textContent =
508
+ `${sensorData.antennas.right.toFixed(2)}, ${sensorData.antennas.left.toFixed(2)}`;
509
+
510
+ const healthPercent = (sensorData.health / 100) * 100;
511
+ const healthColor = healthPercent > 50 ? '#0f0' : healthPercent > 25 ? '#ff0' : '#f00';
512
+ document.getElementById('status').innerHTML = `HP: <span style="color: ${healthColor}">${sensorData.health}</span>`;
513
+ }
514
+
515
+ async function addScore(points) {
516
+ try {
517
+ await fetch('/add_score', {
518
+ method: 'POST',
519
+ headers: { 'Content-Type': 'application/json' },
520
+ body: JSON.stringify({ points })
521
+ });
522
+ } catch (e) {
523
+ console.error('Error adding score:', e);
524
+ }
525
+ }
526
+
527
+ async function damagePlayer(damage) {
528
+ try {
529
+ await fetch('/damage_player', {
530
+ method: 'POST',
531
+ headers: { 'Content-Type': 'application/json' },
532
+ body: JSON.stringify({ damage })
533
+ });
534
+ } catch (e) {
535
+ console.error('Error damaging player:', e);
536
+ }
537
+ }
538
+
539
+ function updateSpaceship() {
540
+ if (!spaceship) return;
541
+
542
+ // Map sensor data to spaceship rotation
543
+ // Pitch: tilt forward/back controls pitch (up/down aiming)
544
+ // Yaw: turn head left/right controls yaw (left/right aiming)
545
+ // Roll: tilt left/right for visual banking effect
546
+ const targetRotation = {
547
+ x: THREE.MathUtils.degToRad(sensorData.pitch * MOVEMENT_SCALE),
548
+ y: THREE.MathUtils.degToRad(sensorData.yaw * MOVEMENT_SCALE), // Yaw controls yaw for left/right aim
549
+ z: THREE.MathUtils.degToRad(sensorData.roll * MOVEMENT_SCALE) // Roll for visual banking
550
+ };
551
+
552
+ // Smooth rotation
553
+ spaceship.rotation.x += (targetRotation.x - spaceship.rotation.x) * 0.1;
554
+ spaceship.rotation.y += (targetRotation.y - spaceship.rotation.y) * 0.1;
555
+ spaceship.rotation.z += (targetRotation.z - spaceship.rotation.z) * 0.1;
556
+
557
+ // Use yaw to also move spaceship left/right in space for dynamic feel
558
+ const targetX = sensorData.yaw * YAW_MOVEMENT_SCALE;
559
+ spaceship.position.x += (targetX - spaceship.position.x) * 0.1;
560
+
561
+ // Limit spaceship movement range
562
+ spaceship.position.x = Math.max(-30, Math.min(30, spaceship.position.x));
563
+
564
+ // Animate engine glow
565
+ if (spaceshipParts.engine) {
566
+ const pulse = Math.sin(Date.now() * 0.01) * 0.1 + 0.9;
567
+ spaceshipParts.engine.material.opacity = pulse;
568
+ spaceshipParts.engineLight.intensity = pulse * 2;
569
+ }
570
+ }
571
+
572
+ function updateAsteroids() {
573
+ for (let i = asteroids.length - 1; i >= 0; i--) {
574
+ const asteroid = asteroids[i];
575
+
576
+ // Move asteroid toward camera (simulating forward movement)
577
+ asteroid.position.x += asteroid.userData.velocity.x;
578
+ asteroid.position.y += asteroid.userData.velocity.y;
579
+ asteroid.position.z += FORWARD_SPEED + asteroid.userData.velocity.z;
580
+
581
+ // Rotate asteroid
582
+ asteroid.rotation.x += asteroid.userData.rotationSpeed.x;
583
+ asteroid.rotation.y += asteroid.userData.rotationSpeed.y;
584
+ asteroid.rotation.z += asteroid.userData.rotationSpeed.z;
585
+
586
+ // Remove if behind camera
587
+ if (asteroid.position.z > 20) {
588
+ scene.remove(asteroid);
589
+ asteroids.splice(i, 1);
590
+ }
591
+ }
592
+ }
593
+
594
+ function updateProjectiles() {
595
+ for (let i = projectiles.length - 1; i >= 0; i--) {
596
+ const projectile = projectiles[i];
597
+
598
+ // Move projectile using velocity vector
599
+ projectile.position.add(projectile.userData.velocity);
600
+
601
+ let hitSomething = false;
602
+
603
+ // Check collision with asteroids
604
+ for (let j = asteroids.length - 1; j >= 0; j--) {
605
+ const asteroid = asteroids[j];
606
+ const distance = projectile.position.distanceTo(asteroid.position);
607
+
608
+ if (distance < asteroid.geometry.parameters.radius + 0.5) {
609
+ scene.remove(asteroid);
610
+ asteroids.splice(j, 1);
611
+
612
+ scene.remove(projectile);
613
+ projectiles.splice(i, 1);
614
+
615
+ createExplosion(projectile.position, 0x888888);
616
+ hitSomething = true;
617
+ break;
618
+ }
619
+ }
620
+
621
+ if (hitSomething) continue;
622
+
623
+ // Check collision with enemies
624
+ for (let j = enemies.length - 1; j >= 0; j--) {
625
+ const enemy = enemies[j];
626
+ const distance = projectile.position.distanceTo(enemy.position);
627
+
628
+ // Larger hitbox based on enemy size
629
+ const hitRadius = 3 + (enemy.userData.type === 2 ? 1 : 0); // Heavy enemies are bigger
630
+ if (distance < hitRadius) {
631
+ enemy.userData.health -= 1;
632
+
633
+ // Flash enemy when hit
634
+ enemy.userData.body.material.emissiveIntensity = 1;
635
+ setTimeout(() => {
636
+ if (enemy.userData.body) {
637
+ enemy.userData.body.material.emissiveIntensity = 0.3;
638
+ }
639
+ }, 100);
640
+
641
+ scene.remove(projectile);
642
+ projectiles.splice(i, 1);
643
+
644
+ if (enemy.userData.health <= 0) {
645
+ createExplosion(enemy.position, enemy.userData.body.material.color);
646
+ scene.remove(enemy);
647
+ enemies.splice(j, 1);
648
+ addScore(enemy.userData.points);
649
+ }
650
+
651
+ hitSomething = true;
652
+ break;
653
+ }
654
+ }
655
+
656
+ if (hitSomething) continue;
657
+
658
+ // Remove if too far from camera
659
+ if (projectile.position.z < -200 || projectile.position.length() > 300) {
660
+ scene.remove(projectile);
661
+ projectiles.splice(i, 1);
662
+ }
663
+ }
664
+ }
665
+
666
+ function updateEnemies() {
667
+ for (let i = enemies.length - 1; i >= 0; i--) {
668
+ const enemy = enemies[i];
669
+
670
+ enemy.userData.timeAlive += 0.016;
671
+
672
+ // Move enemy into position first
673
+ if (!enemy.userData.hasReachedPosition) {
674
+ // Move toward target Z position
675
+ if (enemy.position.z < enemy.userData.targetZ) {
676
+ enemy.position.z += enemy.userData.moveSpeed;
677
+ if (enemy.position.z >= enemy.userData.targetZ) {
678
+ enemy.userData.hasReachedPosition = true;
679
+ enemy.userData.initialX = enemy.position.x;
680
+ enemy.userData.initialY = enemy.position.y;
681
+ }
682
+ }
683
+ } else {
684
+ // Once in position, execute movement pattern
685
+ switch(enemy.userData.movementPattern) {
686
+ case 0: // Horizontal sweep
687
+ enemy.position.x += enemy.userData.moveDirection * 0.2;
688
+ // Bounce off edges
689
+ if (Math.abs(enemy.position.x) > 35) {
690
+ enemy.userData.moveDirection *= -1;
691
+ }
692
+ break;
693
+
694
+ case 1: // Sine wave
695
+ enemy.position.x = enemy.userData.initialX + Math.sin(enemy.userData.timeAlive * 2) * 15;
696
+ enemy.position.y = enemy.userData.initialY + Math.sin(enemy.userData.timeAlive * 1.5) * 8;
697
+ break;
698
+
699
+ case 2: // Circle
700
+ enemy.userData.circleAngle += 0.02 * enemy.userData.moveDirection;
701
+ enemy.position.x = enemy.userData.initialX + Math.cos(enemy.userData.circleAngle) * enemy.userData.circleRadius;
702
+ enemy.position.y = enemy.userData.initialY + Math.sin(enemy.userData.circleAngle) * enemy.userData.circleRadius;
703
+ break;
704
+
705
+ case 3: // Stationary
706
+ // Stay in place
707
+ break;
708
+ }
709
+ }
710
+
711
+ // Rotate enemy slowly
712
+ enemy.rotation.z += 0.01;
713
+
714
+ // Animate engine glow
715
+ if (enemy.userData.engine) {
716
+ const pulse = Math.sin(Date.now() * 0.005 + i) * 0.3 + 0.7;
717
+ enemy.userData.engine.material.opacity = pulse;
718
+ }
719
+
720
+ // Shoot at player only when in position
721
+ if (enemy.userData.hasReachedPosition) {
722
+ const now = Date.now();
723
+ if (now - enemy.userData.lastShootTime > enemy.userData.shootCooldown) {
724
+ createEnemyBullet(enemy, enemy.userData.shootPattern);
725
+ enemy.userData.lastShootTime = now;
726
+ }
727
+ }
728
+
729
+ // Remove if too far away or if player flew past them
730
+ if (enemy.position.z > 30 || enemy.position.length() > 150) {
731
+ scene.remove(enemy);
732
+ enemies.splice(i, 1);
733
+ }
734
+ }
735
+ }
736
+
737
+ function updateEnemyBullets() {
738
+ for (let i = enemyBullets.length - 1; i >= 0; i--) {
739
+ const bullet = enemyBullets[i];
740
+
741
+ // Move bullet
742
+ bullet.position.add(bullet.userData.velocity);
743
+
744
+ // Check collision with player - larger hitbox
745
+ const distance = bullet.position.distanceTo(spaceship.position);
746
+ if (distance < 3.5) { // Increased from 2 to 3.5
747
+ createExplosion(bullet.position, 0xff0000);
748
+ scene.remove(bullet);
749
+ enemyBullets.splice(i, 1);
750
+ damagePlayer(10);
751
+ console.log('Player hit! HP should decrease by 10');
752
+ continue;
753
+ }
754
+
755
+ // Remove if too far
756
+ if (bullet.position.z > 50 || bullet.position.length() > 200) {
757
+ scene.remove(bullet);
758
+ enemyBullets.splice(i, 1);
759
+ }
760
+ }
761
+ }
762
+
763
+ function updateExplosions() {
764
+ for (let i = explosions.length - 1; i >= 0; i--) {
765
+ const particle = explosions[i];
766
+
767
+ particle.position.add(particle.userData.velocity);
768
+ particle.userData.velocity.multiplyScalar(0.95);
769
+ particle.userData.lifetime--;
770
+
771
+ particle.material.opacity = particle.userData.lifetime / 30;
772
+
773
+ if (particle.userData.lifetime <= 0) {
774
+ scene.remove(particle);
775
+ explosions.splice(i, 1);
776
+ }
777
+ }
778
+ }
779
+
780
+ function onWindowResize() {
781
+ camera.aspect = window.innerWidth / window.innerHeight;
782
+ camera.updateProjectionMatrix();
783
+ renderer.setSize(window.innerWidth, window.innerHeight);
784
+ }
785
+
786
+ function updateReticle() {
787
+ // Calculate where the spaceship is aiming based on rotation
788
+ // Create a point 50 units in front, rotated by the spaceship's rotation
789
+ const aimDirection = new THREE.Vector3(0, 0, -50);
790
+
791
+ // Apply spaceship rotation to get aim direction
792
+ const rotationMatrix = new THREE.Matrix4();
793
+ rotationMatrix.makeRotationFromEuler(spaceship.rotation);
794
+ aimDirection.applyMatrix4(rotationMatrix);
795
+
796
+ // Add to spaceship position
797
+ const aimPoint = spaceship.position.clone().add(aimDirection);
798
+
799
+ // Project to screen coordinates
800
+ const screenPosition = aimPoint.clone();
801
+ screenPosition.project(camera);
802
+
803
+ // Convert to pixel coordinates
804
+ const x = (screenPosition.x * 0.5 + 0.5) * window.innerWidth;
805
+ const y = (screenPosition.y * -0.5 + 0.5) * window.innerHeight;
806
+
807
+ // Update crosshair position
808
+ const crosshair = document.querySelector('.crosshair');
809
+ if (crosshair) {
810
+ crosshair.style.left = `${x}px`;
811
+ crosshair.style.top = `${y}px`;
812
+ crosshair.style.transform = 'translate(-50%, -50%)';
813
+ }
814
+ }
815
+
816
+ function animate() {
817
+ requestAnimationFrame(animate);
818
+
819
+ updateSpaceship();
820
+ updateAsteroids();
821
+ updateEnemies();
822
+ updateProjectiles();
823
+ updateEnemyBullets();
824
+ updateExplosions();
825
+ updateReticle();
826
+
827
+ // Move stars to simulate forward movement through space
828
+ stars.forEach(starField => {
829
+ const positions = starField.geometry.attributes.position.array;
830
+ for (let i = 0; i < positions.length; i += 3) {
831
+ positions[i + 2] += FORWARD_SPEED; // Move stars toward us
832
+
833
+ // Wrap stars around when they pass us
834
+ if (positions[i + 2] > 50) {
835
+ positions[i + 2] = -1950;
836
+ positions[i] = (Math.random() - 0.5) * 2000;
837
+ positions[i + 1] = (Math.random() - 0.5) * 2000;
838
+ }
839
+ }
840
+ starField.geometry.attributes.position.needsUpdate = true;
841
+ });
842
+
843
+ renderer.render(scene, camera);
844
+ }
845
+
846
+ // Start the game
847
+ init();
spaceship_game/static/style.css ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ font-family: sans-serif;
3
+ margin: 24px;
4
+ }
5
+
6
+ #sound-btn {
7
+ padding: 10px 20px;
8
+ border: none;
9
+ color: white;
10
+ cursor: pointer;
11
+ font-size: 16px;
12
+ border-radius: 6px;
13
+ background-color: #3498db;
14
+ }
15
+
16
+ #status {
17
+ margin-top: 16px;
18
+ font-weight: bold;
19
+ }
20
+
21
+ #controls {
22
+ display: flex;
23
+ align-items: center;
24
+ gap: 20px;
25
+ }
style.css ADDED
@@ -0,0 +1,411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
9
+ line-height: 1.6;
10
+ color: #333;
11
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
12
+ min-height: 100vh;
13
+ }
14
+
15
+ .hero {
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ color: white;
18
+ padding: 4rem 2rem;
19
+ text-align: center;
20
+ }
21
+
22
+ .hero-content {
23
+ max-width: 800px;
24
+ margin: 0 auto;
25
+ }
26
+
27
+ .app-icon {
28
+ font-size: 4rem;
29
+ margin-bottom: 1rem;
30
+ display: inline-block;
31
+ }
32
+
33
+ .hero h1 {
34
+ font-size: 3rem;
35
+ font-weight: 700;
36
+ margin-bottom: 1rem;
37
+ background: linear-gradient(45deg, #fff, #f0f9ff);
38
+ background-clip: text;
39
+ -webkit-background-clip: text;
40
+ -webkit-text-fill-color: transparent;
41
+ }
42
+
43
+ .tagline {
44
+ font-size: 1.25rem;
45
+ opacity: 0.9;
46
+ max-width: 600px;
47
+ margin: 0 auto;
48
+ }
49
+
50
+ .container {
51
+ max-width: 1200px;
52
+ margin: 0 auto;
53
+ padding: 0 2rem;
54
+ position: relative;
55
+ z-index: 2;
56
+ }
57
+
58
+ .main-card {
59
+ background: white;
60
+ border-radius: 20px;
61
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
62
+ margin-top: -2rem;
63
+ overflow: hidden;
64
+ margin-bottom: 3rem;
65
+ }
66
+
67
+ .app-preview {
68
+ background: linear-gradient(135deg, #1e3a8a, #3b82f6);
69
+ padding: 3rem;
70
+ color: white;
71
+ text-align: center;
72
+ position: relative;
73
+ }
74
+
75
+ .preview-image {
76
+ background: #000;
77
+ border-radius: 15px;
78
+ padding: 2rem;
79
+ max-width: 500px;
80
+ margin: 0 auto;
81
+ position: relative;
82
+ overflow: hidden;
83
+ }
84
+
85
+ .camera-feed {
86
+ font-size: 4rem;
87
+ margin-bottom: 1rem;
88
+ opacity: 0.7;
89
+ }
90
+
91
+ .detection-overlay {
92
+ position: absolute;
93
+ top: 50%;
94
+ left: 50%;
95
+ transform: translate(-50%, -50%);
96
+ width: 100%;
97
+ }
98
+
99
+ .bbox {
100
+ background: rgba(34, 197, 94, 0.9);
101
+ color: white;
102
+ padding: 0.5rem 1rem;
103
+ border-radius: 8px;
104
+ font-size: 0.9rem;
105
+ font-weight: 600;
106
+ margin: 0.5rem;
107
+ display: inline-block;
108
+ border: 2px solid #22c55e;
109
+ }
110
+
111
+ .app-details {
112
+ padding: 3rem;
113
+ }
114
+
115
+ .app-details h2 {
116
+ font-size: 2rem;
117
+ color: #1e293b;
118
+ margin-bottom: 2rem;
119
+ text-align: center;
120
+ }
121
+
122
+ .template-info {
123
+ display: grid;
124
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
125
+ gap: 2rem;
126
+ margin-bottom: 3rem;
127
+ }
128
+
129
+ .info-box {
130
+ background: #f0f9ff;
131
+ border: 2px solid #e0f2fe;
132
+ border-radius: 12px;
133
+ padding: 2rem;
134
+ }
135
+
136
+ .info-box h3 {
137
+ color: #0c4a6e;
138
+ margin-bottom: 1rem;
139
+ font-size: 1.2rem;
140
+ }
141
+
142
+ .info-box p {
143
+ color: #0369a1;
144
+ line-height: 1.6;
145
+ }
146
+
147
+ .how-to-use {
148
+ background: #fefce8;
149
+ border: 2px solid #fde047;
150
+ border-radius: 12px;
151
+ padding: 2rem;
152
+ margin-top: 3rem;
153
+ }
154
+
155
+ .how-to-use h3 {
156
+ color: #a16207;
157
+ margin-bottom: 1.5rem;
158
+ font-size: 1.3rem;
159
+ text-align: center;
160
+ }
161
+
162
+ .steps {
163
+ display: flex;
164
+ flex-direction: column;
165
+ gap: 1.5rem;
166
+ }
167
+
168
+ .step {
169
+ display: flex;
170
+ align-items: flex-start;
171
+ gap: 1rem;
172
+ }
173
+
174
+ .step-number {
175
+ background: #eab308;
176
+ color: white;
177
+ width: 2rem;
178
+ height: 2rem;
179
+ border-radius: 50%;
180
+ display: flex;
181
+ align-items: center;
182
+ justify-content: center;
183
+ font-weight: bold;
184
+ flex-shrink: 0;
185
+ }
186
+
187
+ .step h4 {
188
+ color: #a16207;
189
+ margin-bottom: 0.5rem;
190
+ font-size: 1.1rem;
191
+ }
192
+
193
+ .step p {
194
+ color: #ca8a04;
195
+ }
196
+
197
+ .download-card {
198
+ background: white;
199
+ border-radius: 20px;
200
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
201
+ padding: 3rem;
202
+ text-align: center;
203
+ }
204
+
205
+ .download-card h2 {
206
+ font-size: 2rem;
207
+ color: #1e293b;
208
+ margin-bottom: 1rem;
209
+ }
210
+
211
+ .download-card>p {
212
+ color: #64748b;
213
+ font-size: 1.1rem;
214
+ margin-bottom: 2rem;
215
+ }
216
+
217
+ .dashboard-config {
218
+ margin-bottom: 2rem;
219
+ text-align: left;
220
+ max-width: 400px;
221
+ margin-left: auto;
222
+ margin-right: auto;
223
+ }
224
+
225
+ .dashboard-config label {
226
+ display: block;
227
+ color: #374151;
228
+ font-weight: 600;
229
+ margin-bottom: 0.5rem;
230
+ }
231
+
232
+ .dashboard-config input {
233
+ width: 100%;
234
+ padding: 0.75rem 1rem;
235
+ border: 2px solid #e5e7eb;
236
+ border-radius: 8px;
237
+ font-size: 0.95rem;
238
+ transition: border-color 0.2s;
239
+ }
240
+
241
+ .dashboard-config input:focus {
242
+ outline: none;
243
+ border-color: #667eea;
244
+ }
245
+
246
+ .install-btn {
247
+ background: linear-gradient(135deg, #667eea, #764ba2);
248
+ color: white;
249
+ border: none;
250
+ padding: 1.25rem 3rem;
251
+ border-radius: 16px;
252
+ font-size: 1.2rem;
253
+ font-weight: 700;
254
+ cursor: pointer;
255
+ transition: all 0.3s ease;
256
+ display: inline-flex;
257
+ align-items: center;
258
+ gap: 0.75rem;
259
+ margin-bottom: 2rem;
260
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
261
+ }
262
+
263
+ .install-btn:hover:not(:disabled) {
264
+ transform: translateY(-3px);
265
+ box-shadow: 0 15px 35px rgba(102, 126, 234, 0.4);
266
+ }
267
+
268
+ .install-btn:disabled {
269
+ opacity: 0.7;
270
+ cursor: not-allowed;
271
+ transform: none;
272
+ }
273
+
274
+ .manual-option {
275
+ background: #f8fafc;
276
+ border-radius: 12px;
277
+ padding: 2rem;
278
+ margin-top: 2rem;
279
+ }
280
+
281
+ .manual-option h3 {
282
+ color: #1e293b;
283
+ margin-bottom: 1rem;
284
+ font-size: 1.2rem;
285
+ }
286
+
287
+ .manual-option>p {
288
+ color: #64748b;
289
+ margin-bottom: 1rem;
290
+ }
291
+
292
+ .btn-icon {
293
+ font-size: 1.1rem;
294
+ }
295
+
296
+ .install-status {
297
+ padding: 1rem;
298
+ border-radius: 8px;
299
+ font-size: 0.9rem;
300
+ text-align: center;
301
+ display: none;
302
+ margin-top: 1rem;
303
+ }
304
+
305
+ .install-status.success {
306
+ background: #dcfce7;
307
+ color: #166534;
308
+ border: 1px solid #bbf7d0;
309
+ }
310
+
311
+ .install-status.error {
312
+ background: #fef2f2;
313
+ color: #dc2626;
314
+ border: 1px solid #fecaca;
315
+ }
316
+
317
+ .install-status.loading {
318
+ background: #dbeafe;
319
+ color: #1d4ed8;
320
+ border: 1px solid #bfdbfe;
321
+ }
322
+
323
+ .install-status.info {
324
+ background: #e0f2fe;
325
+ color: #0369a1;
326
+ border: 1px solid #7dd3fc;
327
+ }
328
+
329
+ .manual-install {
330
+ background: #1f2937;
331
+ border-radius: 8px;
332
+ padding: 1rem;
333
+ margin-bottom: 1rem;
334
+ display: flex;
335
+ align-items: center;
336
+ gap: 1rem;
337
+ }
338
+
339
+ .manual-install code {
340
+ color: #10b981;
341
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
342
+ font-size: 0.85rem;
343
+ flex: 1;
344
+ overflow-x: auto;
345
+ }
346
+
347
+ .copy-btn {
348
+ background: #374151;
349
+ color: white;
350
+ border: none;
351
+ padding: 0.5rem 1rem;
352
+ border-radius: 6px;
353
+ font-size: 0.8rem;
354
+ cursor: pointer;
355
+ transition: background-color 0.2s;
356
+ }
357
+
358
+ .copy-btn:hover {
359
+ background: #4b5563;
360
+ }
361
+
362
+ .manual-steps {
363
+ color: #6b7280;
364
+ font-size: 0.9rem;
365
+ line-height: 1.8;
366
+ }
367
+
368
+ .footer {
369
+ text-align: center;
370
+ padding: 2rem;
371
+ color: white;
372
+ opacity: 0.8;
373
+ }
374
+
375
+ .footer a {
376
+ color: white;
377
+ text-decoration: none;
378
+ font-weight: 600;
379
+ }
380
+
381
+ .footer a:hover {
382
+ text-decoration: underline;
383
+ }
384
+
385
+ /* Responsive Design */
386
+ @media (max-width: 768px) {
387
+ .hero {
388
+ padding: 2rem 1rem;
389
+ }
390
+
391
+ .hero h1 {
392
+ font-size: 2rem;
393
+ }
394
+
395
+ .container {
396
+ padding: 0 1rem;
397
+ }
398
+
399
+ .app-details,
400
+ .download-card {
401
+ padding: 2rem;
402
+ }
403
+
404
+ .features-grid {
405
+ grid-template-columns: 1fr;
406
+ }
407
+
408
+ .download-options {
409
+ grid-template-columns: 1fr;
410
+ }
411
+ }