1 /*
<lambda>null2  * Copyright (C) 2024 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 kotlin.math.min
20 import kotlin.math.sign
21 
22 class Autopilot(val ship: Spacecraft, val universe: Universe) : Entity {
23     val BRAKING_TIME = 5f
24     val SIGHTSEEING_TIME = 15f
25     val LAUNCH_THRUST_TIME = 5f
26     val STRATEGY_MIN_TIME = 0.5f
27 
28     var enabled = false
29 
30     var target: Planet? = null
31 
32     var landingAltitude = 0f
33 
34     var nextStrategyTime = 0f
35 
36     var brakingDistance = 0f
37 
38     // used by rendering
39     var leadingPos = Vec2.Zero
40     var leadingVector = Vec2.Zero
41 
42     val telemetry: String
43         get() =
44             listOf(
45                     "---- AUTOPILOT ENGAGED ----",
46                     "TGT: " + (target?.name?.toUpperCase() ?: "SELECTING..."),
47                     "EXE: $strategy" + if (debug.isNotEmpty()) " ($debug)" else "",
48                 )
49                 .joinToString("\n")
50 
51     private var strategy: String = "NONE"
52     private var debug: String = ""
53 
54     override fun update(sim: Simulator, dt: Float) {
55         if (!enabled) return
56 
57         if (sim.now < nextStrategyTime) {
58             return
59         }
60 
61         val currentStrategy = strategy
62 
63         if (ship.landing != null) {
64             if (target != null) {
65                 strategy = "LANDED"
66                 debug = ""
67                 // we just got here. see the sights.
68                 target = null
69                 landingAltitude = 0f
70                 nextStrategyTime = sim.now + SIGHTSEEING_TIME
71             } else {
72                 // full power until we blast off
73                 ship.thrust = Vec2.makeWithAngleMag(ship.angle, 1f)
74 
75                 strategy = "LAUNCHING"
76                 debug = ""
77                 nextStrategyTime = sim.now + LAUNCH_THRUST_TIME
78             }
79         } else {
80             // select new target
81 
82             if (target == null) {
83                 // testing: target the first planet
84                 //   target = universe.planets[0]
85 
86                 // target the nearest unexplored planet
87                 target =
88                     universe.planets
89                         .sortedBy { (it.pos - ship.pos).mag() }
90                         .firstOrNull { !it.explored }
91                 brakingDistance = 0f
92 
93                 // if we've explored them all, pick one at random
94                 if (target == null) target = universe.planets.random()
95             }
96 
97             target?.let { target -> // should be nonnull
98                 val shipV = ship.velocity
99                 val targetV = target.velocity
100                 val targetVector = (target.pos - ship.pos)
101                 val altitude = targetVector.mag() - target.radius
102 
103                 landingAltitude = min(target.radius, 100f)
104 
105                 // the following is in the moving reference frame of the target
106                 val relativeV: Vec2 = shipV - targetV
107                 val projection = relativeV.dot(targetVector / targetVector.mag())
108                 val relativeSpeed = relativeV.mag() * projection.sign
109                 val timeToTarget = if (relativeSpeed != 0f) altitude / relativeSpeed else 1_000f
110 
111                 val newBrakingDistance =
112                     BRAKING_TIME * if (relativeSpeed > 0) relativeSpeed else MAIN_ENGINE_ACCEL
113                 brakingDistance =
114                     expSmooth(brakingDistance, newBrakingDistance, dt = sim.dt, speed = 5f)
115 
116                 // We're going to aim at where the target will be, but we want to make sure to
117                 // compute
118                 leadingPos =
119                     target.pos +
120                         Vec2.makeWithAngleMag(
121                             target.velocity.angle(),
122                             min(altitude / 2, target.velocity.mag())
123                         )
124                 leadingVector = leadingPos - ship.pos
125 
126                 if (altitude < landingAltitude) {
127                     strategy = "LANDING"
128                     // Strategy: zero thrust, face away, prepare for landing
129 
130                     ship.angle = (ship.pos - target.pos).angle() // point away from ground
131                     ship.thrust = Vec2.Zero
132                 } else {
133                     if (relativeSpeed < 0 || altitude > brakingDistance) {
134                         strategy = "CHASING"
135                         // Strategy: Make tracks. We are either a long way away, or falling behind.
136                         ship.angle = leadingVector.angle()
137 
138                         ship.thrust = Vec2.makeWithAngleMag(ship.angle, 1.0f)
139                     } else {
140                         strategy = "APPROACHING"
141                         // Strategy: Just slow down. If we get caught in the gravity well, it will
142                         // gradually start pulling us more in the direction of the planet, which
143                         // will create a graceful deceleration
144                         ship.angle = (-ship.velocity).angle()
145 
146                         // We want to bleed off velocity over time. Specifically, relativeSpeed px/s
147                         // over timeToTarget seconds.
148                         val decel = relativeSpeed / timeToTarget
149                         val decelThrust =
150                             decel / MAIN_ENGINE_ACCEL * 0.9f // not quite slowing down enough
151                         ship.thrust = Vec2.makeWithAngleMag(ship.angle, decelThrust)
152                     }
153                 }
154                 debug = ("DV=%.0f D=%.0f T%+.1f").format(relativeSpeed, altitude, timeToTarget)
155             }
156             if (strategy != currentStrategy) {
157                 nextStrategyTime = sim.now + STRATEGY_MIN_TIME
158             }
159         }
160     }
161 
162     override fun postUpdate(sim: Simulator, dt: Float) {
163         if (!enabled) return
164     }
165 }
166