1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.egg.landroid
18 
19 import android.util.ArraySet
20 import androidx.compose.ui.graphics.Color
21 import androidx.compose.ui.util.lerp
22 import kotlin.math.absoluteValue
23 import kotlin.math.pow
24 import kotlin.math.sqrt
25 
26 const val UNIVERSE_RANGE = 200_000f
27 
28 val NUM_PLANETS_RANGE = 1..10
29 val STAR_RADIUS_RANGE = (1_000f..8_000f)
30 val PLANET_RADIUS_RANGE = (50f..2_000f)
31 val PLANET_ORBIT_RANGE = (STAR_RADIUS_RANGE.endInclusive * 2f)..(UNIVERSE_RANGE * 0.75f)
32 
33 const val GRAVITATION = 1e-2f
34 const val KEPLER_CONSTANT = 50f // * 4f * PIf * PIf / GRAVITATION
35 
36 // m = d * r
37 const val PLANETARY_DENSITY = 2.5f
38 const val STELLAR_DENSITY = 0.5f
39 
40 const val SPACECRAFT_MASS = 10f
41 
42 const val CRAFT_SPEED_LIMIT = 5_000f
43 const val MAIN_ENGINE_ACCEL = 1000f // thrust effect, pixels per second squared
44 const val LAUNCH_MECO = 2f // how long to suspend gravity when launching
45 
46 const val SCALED_THRUST = true
47 
48 interface Removable {
49     fun canBeRemoved(): Boolean
50 }
51 
52 open class Planet(
53     val orbitCenter: Vec2,
54     radius: Float,
55     pos: Vec2,
56     var speed: Float,
57     var color: Color = Color.White
58 ) : Body() {
59     var atmosphere = ""
60     var description = ""
61     var flora = ""
62     var fauna = ""
63     var explored = false
64     private val orbitRadius: Float
65     init {
66         this.radius = radius
67         this.pos = pos
68         orbitRadius = pos.distance(orbitCenter)
69         mass = 4 / 3 * PIf * radius.pow(3) * PLANETARY_DENSITY
70     }
71 
72     override fun update(sim: Simulator, dt: Float) {
73         val orbitAngle = (pos - orbitCenter).angle()
74         // constant linear velocity
75         velocity = Vec2.makeWithAngleMag(orbitAngle + PIf / 2f, speed)
76 
77         super.update(sim, dt)
78     }
79 
80     override fun postUpdate(sim: Simulator, dt: Float) {
81         // This is kind of like a constraint, but whatever.
82         val orbitAngle = (pos - orbitCenter).angle()
83         pos = orbitCenter + Vec2.makeWithAngleMag(orbitAngle, orbitRadius)
84         super.postUpdate(sim, dt)
85     }
86 }
87 
88 enum class StarClass {
89     O,
90     B,
91     A,
92     F,
93     G,
94     K,
95     M
96 }
97 
98 fun starColor(cls: StarClass) =
99     when (cls) {
100         StarClass.O -> Color(0xFF6666FF)
101         StarClass.B -> Color(0xFFCCCCFF)
102         StarClass.A -> Color(0xFFEEEEFF)
103         StarClass.F -> Color(0xFFFFFFFF)
104         StarClass.G -> Color(0xFFFFFF66)
105         StarClass.K -> Color(0xFFFFCC33)
106         StarClass.M -> Color(0xFFFF8800)
107     }
108 
109 class Star(val cls: StarClass, radius: Float) :
110     Planet(orbitCenter = Vec2.Zero, radius = radius, pos = Vec2.Zero, speed = 0f) {
111     init {
112         pos = Vec2.Zero
113         mass = 4 / 3 * PIf * radius.pow(3) * STELLAR_DENSITY
114         color = starColor(cls)
115         collides = false
116     }
117     var anim = 0f
118     override fun update(sim: Simulator, dt: Float) {
119         anim += dt
120     }
121 }
122 
123 open class Universe(val namer: Namer, randomSeed: Long) : Simulator(randomSeed) {
124     var latestDiscovery: Planet? = null
125     lateinit var star: Star
126     lateinit var ship: Spacecraft
127     val planets: MutableList<Planet> = mutableListOf()
128     var follow: Body? = null
129     val ringfence = Container(UNIVERSE_RANGE)
130 
131     fun initTest() {
132         val systemName = "TEST SYSTEM"
133         star =
134             Star(
135                     cls = StarClass.A,
136                     radius = STAR_RADIUS_RANGE.endInclusive,
137                 )
138                 .apply { name = "TEST SYSTEM" }
139 
140         repeat(NUM_PLANETS_RANGE.last) {
141             val thisPlanetFrac = it.toFloat() / (NUM_PLANETS_RANGE.last - 1)
142             val radius =
143                 lerp(PLANET_RADIUS_RANGE.start, PLANET_RADIUS_RANGE.endInclusive, thisPlanetFrac)
144             val orbitRadius =
145                 lerp(PLANET_ORBIT_RANGE.start, PLANET_ORBIT_RANGE.endInclusive, thisPlanetFrac)
146 
147             val period = sqrt(orbitRadius.pow(3f) / star.mass) * KEPLER_CONSTANT
148             val speed = 2f * PIf * orbitRadius / period
149 
150             val p =
151                 Planet(
152                     orbitCenter = star.pos,
153                     radius = radius,
154                     pos = star.pos + Vec2.makeWithAngleMag(thisPlanetFrac * PI2f, orbitRadius),
155                     speed = speed,
156                     color = Colors.Eigengrau4
157                 )
158             android.util.Log.v(
159                 "Landroid",
160                 "created planet $p with period $period and vel $speed"
161             )
162             val num = it + 1
163             p.description = "TEST PLANET #$num"
164             p.atmosphere = "radius=$radius"
165             p.flora = "mass=${p.mass}"
166             p.fauna = "speed=$speed"
167             planets.add(p)
168             add(p)
169         }
170 
171         planets.sortBy { it.pos.distance(star.pos) }
172         planets.forEachIndexed { idx, planet -> planet.name = "$systemName ${idx + 1}" }
173         add(star)
174 
175         ship = Spacecraft()
176 
177         ship.pos = star.pos + Vec2.makeWithAngleMag(PIf / 4, PLANET_ORBIT_RANGE.start)
178         ship.angle = 0f
179         add(ship)
180 
181         ringfence.add(ship)
182         add(ringfence)
183 
184         follow = ship
185     }
186 
187     fun initRandom() {
188         val systemName = namer.nameSystem(rng)
189         star =
190             Star(
191                 cls = rng.choose(StarClass.values()),
192                 radius = rng.nextFloatInRange(STAR_RADIUS_RANGE)
193             )
194         star.name = systemName
195         repeat(rng.nextInt(NUM_PLANETS_RANGE.first, NUM_PLANETS_RANGE.last + 1)) {
196             val radius = rng.nextFloatInRange(PLANET_RADIUS_RANGE)
197             val orbitRadius =
198                 lerp(
199                     PLANET_ORBIT_RANGE.start,
200                     PLANET_ORBIT_RANGE.endInclusive,
201                     rng.nextFloat().pow(1f)
202                 )
203 
204             // Kepler's third law
205             val period = sqrt(orbitRadius.pow(3f) / star.mass) * KEPLER_CONSTANT
206             val speed = 2f * PIf * orbitRadius / period
207 
208             val p =
209                 Planet(
210                     orbitCenter = star.pos,
211                     radius = radius,
212                     pos = star.pos + Vec2.makeWithAngleMag(rng.nextFloat() * PI2f, orbitRadius),
213                     speed = speed,
214                     color = Colors.Eigengrau4
215                 )
216             android.util.Log.v(
217                 "Landroid",
218                 "created planet $p with period $period and vel $speed"
219             )
220             p.description = namer.describePlanet(rng)
221             p.atmosphere = namer.describeAtmo(rng)
222             p.flora = namer.describeLife(rng)
223             p.fauna = namer.describeLife(rng)
224             planets.add(p)
225             add(p)
226         }
227         planets.sortBy { it.pos.distance(star.pos) }
228         planets.forEachIndexed { idx, planet -> planet.name = "$systemName ${idx + 1}" }
229         add(star)
230 
231         ship = Spacecraft()
232 
233         ship.pos =
234             star.pos +
235                 Vec2.makeWithAngleMag(
236                     rng.nextFloat() * PI2f,
237                     rng.nextFloatInRange(PLANET_ORBIT_RANGE.start, PLANET_ORBIT_RANGE.endInclusive)
238                 )
239         ship.angle = rng.nextFloat() * PI2f
240         add(ship)
241 
242         ringfence.add(ship)
243         add(ringfence)
244 
245         follow = ship
246     }
247 
248     override fun updateAll(dt: Float, entities: ArraySet<Entity>) {
249         // check for passing in front of the sun
250         ship.transit = false
251 
252         (planets + star).forEach { planet ->
253             val vector = planet.pos - ship.pos
254             val d = vector.mag()
255             if (d < planet.radius) {
256                 if (planet is Star) ship.transit = true
257             } else if (
258                 now > ship.launchClock + LAUNCH_MECO
259             ) { // within MECO sec of launch, no gravity at all
260                 // simulate gravity: $ f_g = G * m1 * m2 * 1/d^2 $
261                 ship.velocity =
262                     ship.velocity +
263                         Vec2.makeWithAngleMag(
264                             vector.angle(),
265                             GRAVITATION * (ship.mass * planet.mass) / d.pow(2)
266                         ) * dt
267             }
268         }
269 
270         super.updateAll(dt, entities)
271     }
272 
273     fun closestPlanet(): Planet {
274         val bodiesByDist =
275             (planets + star)
276                 .map { planet -> (planet.pos - ship.pos) to planet }
277                 .sortedBy { it.first.mag() }
278 
279         return bodiesByDist[0].second
280     }
281 
282     override fun solveAll(dt: Float, constraints: ArraySet<Constraint>) {
283         if (ship.landing == null) {
284             val planet = closestPlanet()
285 
286             if (planet.collides) {
287                 val d = (ship.pos - planet.pos).mag() - ship.radius - planet.radius
288                 val a = (ship.pos - planet.pos).angle()
289 
290                 if (d < 0) {
291                     // landing, or impact?
292 
293                     // 1. relative speed
294                     val vDiff = (ship.velocity - planet.velocity).mag()
295                     // 2. landing angle
296                     val aDiff = (ship.angle - a).absoluteValue
297 
298                     // landing criteria
299                     if (aDiff < PIf / 4
300                     //                        &&
301                     //                        vDiff < 100f
302                     ) {
303                         val landing = Landing(ship, planet, a)
304                         ship.landing = landing
305                         ship.velocity = planet.velocity
306                         add(landing)
307 
308                         planet.explored = true
309                         latestDiscovery = planet
310                     } else {
311                         val impact = planet.pos + Vec2.makeWithAngleMag(a, planet.radius)
312                         ship.pos =
313                             planet.pos + Vec2.makeWithAngleMag(a, planet.radius + ship.radius - d)
314 
315                         //                        add(Spark(
316                         //                            lifetime = 1f,
317                         //                            style = Spark.Style.DOT,
318                         //                            color = Color.Yellow,
319                         //                            size = 10f
320                         //                        ).apply {
321                         //                            pos = impact
322                         //                            opos = impact
323                         //                            velocity = Vec2.Zero
324                         //                        })
325                         //
326                         (1..10).forEach {
327                             Spark(
328                                     lifetime = rng.nextFloatInRange(0.5f, 2f),
329                                     style = Spark.Style.DOT,
330                                     color = Color.White,
331                                     size = 1f
332                                 )
333                                 .apply {
334                                     pos =
335                                         impact +
336                                             Vec2.makeWithAngleMag(
337                                                 rng.nextFloatInRange(0f, 2 * PIf),
338                                                 rng.nextFloatInRange(0.1f, 0.5f)
339                                             )
340                                     opos = pos
341                                     velocity =
342                                         ship.velocity * 0.8f +
343                                             Vec2.makeWithAngleMag(
344                                                 //                                            a +
345                                                 // rng.nextFloatInRange(-PIf, PIf),
346                                                 rng.nextFloatInRange(0f, 2 * PIf),
347                                                 rng.nextFloatInRange(0.1f, 0.5f)
348                                             )
349                                     add(this)
350                                 }
351                         }
352                     }
353                 }
354             }
355         }
356 
357         super.solveAll(dt, constraints)
358     }
359 
360     override fun postUpdateAll(dt: Float, entities: ArraySet<Entity>) {
361         super.postUpdateAll(dt, entities)
362 
363         entities
364             .filterIsInstance<Removable>()
365             .filter(predicate = Removable::canBeRemoved)
366             .filterIsInstance<Entity>()
367             .forEach { remove(it) }
368     }
369 }
370 
371 class Landing(val ship: Spacecraft, val planet: Planet, val angle: Float) : Constraint {
372     private val landingVector = Vec2.makeWithAngleMag(angle, ship.radius + planet.radius)
373     override fun solve(sim: Simulator, dt: Float) {
374         val desiredPos = planet.pos + landingVector
375         ship.pos = (ship.pos * 0.5f) + (desiredPos * 0.5f) // @@@ FIXME
376         ship.angle = angle
377     }
378 }
379 
380 class Spark(
381     var lifetime: Float,
382     collides: Boolean = false,
383     mass: Float = 0f,
384     val style: Style = Style.LINE,
385     val color: Color = Color.Gray,
386     val size: Float = 2f
387 ) : Removable, Body() {
388     enum class Style {
389         LINE,
390         LINE_ABSOLUTE,
391         DOT,
392         DOT_ABSOLUTE,
393         RING
394     }
395 
396     init {
397         this.collides = collides
398         this.mass = mass
399     }
400     override fun update(sim: Simulator, dt: Float) {
401         super.update(sim, dt)
402         lifetime -= dt
403     }
404     override fun canBeRemoved(): Boolean {
405         return lifetime < 0
406     }
407 }
408 
409 const val TRACK_LENGTH = 10_000
410 const val SIMPLE_TRACK_DRAWING = true
411 
412 class Track {
413     val positions = ArrayDeque<Vec2>(TRACK_LENGTH)
414     private val angles = ArrayDeque<Float>(TRACK_LENGTH)
415     fun add(x: Float, y: Float, a: Float) {
416         if (positions.size >= (TRACK_LENGTH - 1)) {
417             positions.removeFirst()
418             angles.removeFirst()
419             positions.removeFirst()
420             angles.removeFirst()
421         }
422         positions.addLast(Vec2(x, y))
423         angles.addLast(a)
424     }
425 }
426 
427 class Spacecraft : Body() {
428     var thrust = Vec2.Zero
429     var launchClock = 0f
430 
431     var transit = false
432 
433     val track = Track()
434 
435     var landing: Landing? = null
436 
437     init {
438         mass = SPACECRAFT_MASS
439         radius = 12f
440     }
441 
442     override fun update(sim: Simulator, dt: Float) {
443         // check for thrusters
444         val thrustMag = thrust.mag()
445         if (thrustMag > 0) {
446             var deltaV = MAIN_ENGINE_ACCEL * dt
447             if (SCALED_THRUST) deltaV *= thrustMag.coerceIn(0f, 1f)
448 
449             if (landing == null) {
450                 // we are free in space, so we attempt to pivot toward the desired direction
451                 // NOTE: no longer required thanks to FlightStick
452                 // angle = thrust.angle()
453             } else
454                 landing?.let { landing ->
455                     if (launchClock == 0f) launchClock = sim.now + 1f /* @@@ TODO extract */
456 
457                     if (sim.now > launchClock) {
458                         // first-stage to orbit has 1000x power
459                         //                    deltaV *= 1000f
460                         sim.remove(landing)
461                         this.landing = null
462                     } else {
463                         deltaV = 0f
464                     }
465                 }
466 
467             // this is it. impart thrust to the ship.
468             // note that we always thrust in the forward direction
469             velocity += Vec2.makeWithAngleMag(angle, deltaV)
470         } else {
471             if (launchClock != 0f) launchClock = 0f
472         }
473 
474         // apply global speed limit
475         if (velocity.mag() > CRAFT_SPEED_LIMIT)
476             velocity = Vec2.makeWithAngleMag(velocity.angle(), CRAFT_SPEED_LIMIT)
477 
478         super.update(sim, dt)
479     }
480 
481     override fun postUpdate(sim: Simulator, dt: Float) {
482         super.postUpdate(sim, dt)
483 
484         // special effects all need to be added after the simulation step so they have
485         // the correct position of the ship.
486         track.add(pos.x, pos.y, angle)
487 
488         val mag = thrust.mag()
489         if (sim.rng.nextFloat() < mag) {
490             // exhaust
491             sim.add(
492                 Spark(
493                         lifetime = sim.rng.nextFloatInRange(0.5f, 1f),
494                         collides = true,
495                         mass = 1f,
496                         style = Spark.Style.RING,
497                         size = 3f,
498                         color = Color(0x40FFFFFF)
499                     )
500                     .also { spark ->
501                         spark.pos = pos
502                         spark.opos = pos
503                         spark.velocity =
504                             velocity +
505                                 Vec2.makeWithAngleMag(
506                                     angle + sim.rng.nextFloatInRange(-0.2f, 0.2f),
507                                     -MAIN_ENGINE_ACCEL * mag * 10f * dt
508                                 )
509                     }
510             )
511         }
512     }
513 }
514