ANDROID 15 MISSION UPDATE -- "VANILLA ICE CREAM"

// UPDATES

  + IDLE DIVERSIONS
  + "AI" AUTOPILOT
  + TYPOGRAPHIC COMPRESSION
  + OCCUPATIONAL DESCRIPTIONS
  + RENDERING TO THE EDGE OF SPACE
  + FLAG SUPPORT: LANDED

// TECHNICAL NOTE //

  This brings back the old enable-components trick; once
  the egg has been invoked, the daydream mode will be
  unlocked for use and will appear in Settings > Display >
  Screen saver, or wherever you get your Dreams.

A PRODUCT OF YOYODROID PROPULSION SYSTEMS
"WHERE INTEGRATION COMPLETE IS TOMORROW!"

Fixes: 330919557
Bug: 320695719
Flag: com.android.egg.flags.flag_flag
Test: easter egg main entry point:
      adb shell am start -n android/com.android.internal.app.PlatLogoActivity
Test: interactive mode:
      adb shell am start -n com.android.egg/.landroid.MainActivity
Test: screensaver mode:
      # first, enable the component if you haven't gone
      # through the main entry point
      adb root ; adb shell pm enable com.android.egg/.landroid.DreamUniverse
      # select the screensaver
      adb shell settings put secure screensaver_components \
          com.android.egg.landroid/com.android.egg.landroid.DreamUniverse
      # start dreaming
      adb shell service call dreams 1
Change-Id: I57a8834ce7f3d41aacaa56133e6f512cf81f5346
diff --git a/packages/EasterEgg/AndroidManifest.xml b/packages/EasterEgg/AndroidManifest.xml
index d1db237..1500583 100644
--- a/packages/EasterEgg/AndroidManifest.xml
+++ b/packages/EasterEgg/AndroidManifest.xml
@@ -36,8 +36,28 @@
         android:icon="@drawable/android14_patch_adaptive"
         android:label="@string/app_name">
 
-        <!-- Android U easter egg -->
+        <!-- Android V easter egg: Daydream version of Landroid
+             (must be enabled by unlocking the egg) -->
+        <service
+            android:name=".landroid.DreamUniverse"
+            android:exported="true"
+            android:icon="@drawable/android14_patch_adaptive"
+            android:label="@string/v_egg_name"
+            android:description="@string/dream_description"
+            android:enabled="false"
+            android:permission="android.permission.BIND_DREAM_SERVICE"
+            >
 
+            <intent-filter>
+                <action android:name="android.service.dreams.DreamService" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data
+                android:name="android.service.dream"
+                android:resource="@xml/landroid_dream"/>
+        </service>
+
+        <!-- Android U easter egg -->
         <activity
             android:name=".landroid.MainActivity"
             android:exported="true"
@@ -52,7 +72,6 @@
             </intent-filter>
         </activity>
 
-
         <!-- Android Q easter egg -->
         <activity
             android:name=".quares.QuaresActivity"
diff --git a/packages/EasterEgg/res/values/landroid_strings.xml b/packages/EasterEgg/res/values/landroid_strings.xml
index 1394f2f..1bbfcca 100644
--- a/packages/EasterEgg/res/values/landroid_strings.xml
+++ b/packages/EasterEgg/res/values/landroid_strings.xml
@@ -1,21 +1,13 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-    Copyright (C) 2023 The Android Open Source Project
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-         http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
--->
-
+<?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <string name="u_egg_name" translatable="false">Android 14 Easter Egg</string>
+
+    <!-- No Android's Sky -->
+    <!-- Char Star Field -->
+    <!-- V-leet: Harmless -->
+    <!-- Contemplating My Orbital Mechanics -->
+    <string name="u_egg_name" translatable="false">Landroid</string>
+    <string name="v_egg_name" translatable="false">Landroid</string>
+    <string name="dream_description" translatable="false">---- AUTOPILOT ENGAGED ----</string>
 
     <string-array name="planet_descriptors" translatable="false">
         <item>earthy</item>
@@ -365,7 +357,64 @@
         <item>relaxed</item>
         <item>skunky</item>
         <item>breezy</item>
-        <item>soup </item>
+        <item>soup</item>
+    </string-array>
+
+    <string-array name="fauna_generic_plurals" translatable="false">
+        <item>fauna</item>
+        <item>animals</item>
+        <item>locals</item>
+        <item>creatures</item>
+        <item>critters</item>
+        <item>wildlife</item>
+        <item>specimens</item>
+        <item>life</item>
+        <item>cells</item>
+    </string-array>
+
+    <string-array name="flora_generic_plurals" translatable="false">
+        <item>flora</item>
+        <item>plants</item>
+        <item>flowers</item>
+        <item>trees</item>
+        <item>mosses</item>
+        <item>specimens</item>
+        <item>life</item>
+        <item>cells</item>
+    </string-array>
+
+    <string-array name="atmo_generic_plurals" translatable="false">
+        <item>air</item>
+        <item>atmosphere</item>
+        <item>clouds</item>
+        <item>atmo</item>
+        <item>gases</item>
+    </string-array>
+
+    <string-array name="activities" translatable="false">
+        <item>refueling</item>
+        <item>sightseeing</item>
+        <item>vacationing</item>
+        <item>luncheoning</item>
+        <item>recharging</item>
+        <item>taking up space</item>
+        <item>reticulating space splines</item>
+        <item>using facilities</item>
+        <item>spelunking</item>
+        <item>repairing</item>
+        <item>herding {fauna}</item>
+        <item>taming {fauna}</item>
+        <item>breeding {fauna}</item>
+        <item>singing lullabies to {fauna}</item>
+        <item>singing lullabies to {flora}</item>
+        <item>singing lullabies to the {planet}</item>
+        <item>gardening {flora}</item>
+        <item>collecting {flora}</item>
+        <item>surveying the {planet}</item>
+        <item>mapping the {planet}</item>
+        <item>breathing {atmo}</item>
+        <item>reprocessing {atmo}</item>
+        <item>bottling {atmo}</item>
     </string-array>
 
 </resources>
diff --git a/packages/EasterEgg/res/xml/landroid_dream.xml b/packages/EasterEgg/res/xml/landroid_dream.xml
new file mode 100644
index 0000000..adf82bd
--- /dev/null
+++ b/packages/EasterEgg/res/xml/landroid_dream.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2024 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<dream xmlns:android="http://schemas.android.com/apk/res/android"
+    android:previewImage="@*android:drawable/platlogo" />
diff --git a/packages/EasterEgg/src/com/android/egg/ComponentActivationActivity.java b/packages/EasterEgg/src/com/android/egg/ComponentActivationActivity.java
index 5820b5a..30320d6 100644
--- a/packages/EasterEgg/src/com/android/egg/ComponentActivationActivity.java
+++ b/packages/EasterEgg/src/com/android/egg/ComponentActivationActivity.java
@@ -18,11 +18,14 @@
 
 import android.app.Activity;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.pm.PackageManager;
 import android.provider.Settings;
 import android.util.Log;
 import android.widget.Toast;
 
+import com.android.egg.flags.Flags;
+import com.android.egg.landroid.DreamUniverse;
 import com.android.egg.neko.NekoControlsService;
 import com.android.egg.widget.PaintChipsActivity;
 import com.android.egg.widget.PaintChipsWidget;
@@ -33,7 +36,9 @@
 public class ComponentActivationActivity extends Activity {
     private static final String TAG = "EasterEgg";
 
+    // check PlatLogoActivity.java for these
     private static final String S_EGG_UNLOCK_SETTING = "egg_mode_s";
+    private static final String V_EGG_UNLOCK_SETTING = "egg_mode_v";
 
     private void toastUp(String s) {
         Toast toast = Toast.makeText(this, s, Toast.LENGTH_SHORT);
@@ -44,14 +49,39 @@
     public void onStart() {
         super.onStart();
 
-        final PackageManager pm = getPackageManager();
-        final ComponentName[] cns = new ComponentName[] {
-                new ComponentName(this, NekoControlsService.class),
-                new ComponentName(this, PaintChipsActivity.class),
-                new ComponentName(this, PaintChipsWidget.class)
-        };
-        final long unlockValue = Settings.System.getLong(getContentResolver(),
-                S_EGG_UNLOCK_SETTING, 0);
+        lockUnlockComponents(this);
+
+        finish();
+    }
+
+    /**
+     * Check easter egg unlock state and update unlockable components to match.
+     */
+    public static void lockUnlockComponents(Context context) {
+        final PackageManager pm = context.getPackageManager();
+        final ComponentName[] cns;
+        final String unlockSettingsKey;
+        final boolean shouldReLock;
+        final long unlockValue;
+        if (Flags.flagFlag()) {
+            unlockSettingsKey = V_EGG_UNLOCK_SETTING;
+            unlockValue = 1; // since we're not toggling we actually don't need to check the setting
+            shouldReLock = false;
+            cns = new ComponentName[]{
+                    new ComponentName(context, DreamUniverse.class)
+            };
+        } else {
+            unlockSettingsKey = S_EGG_UNLOCK_SETTING;
+            unlockValue = Settings.System.getLong(context.getContentResolver(),
+                    unlockSettingsKey, 0);
+            shouldReLock = true;
+            cns = new ComponentName[]{
+                    new ComponentName(context, NekoControlsService.class),
+                    new ComponentName(context, PaintChipsActivity.class),
+                    new ComponentName(context, PaintChipsWidget.class),
+                    new ComponentName(context, DreamUniverse.class)
+            };
+        }
         for (ComponentName cn : cns) {
             final boolean componentEnabled = pm.getComponentEnabledSetting(cn)
                     == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
@@ -77,7 +107,5 @@
                 }
             }
         }
-
-        finish();
     }
 }
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt
new file mode 100644
index 0000000..f71abee
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.egg.landroid
+
+import kotlin.math.min
+import kotlin.math.sign
+
+class Autopilot(val ship: Spacecraft, val universe: Universe) : Entity {
+    val BRAKING_TIME = 5f
+    val SIGHTSEEING_TIME = 10f
+    val STRATEGY_MIN_TIME = 0.5f
+
+    var enabled = false
+
+    var target: Planet? = null
+
+    var landingAltitude = 0f
+
+    var nextStrategyTime = 0f
+
+    var brakingDistance = 0f
+
+    // used by rendering
+    var leadingPos = Vec2.Zero
+    var leadingVector = Vec2.Zero
+
+    val telemetry: String
+        get() =
+            listOf(
+                    "---- AUTOPILOT ENGAGED ----",
+                    "TGT: " + (target?.name?.toUpperCase() ?: "SELECTING..."),
+                    "EXE: $strategy" + if (debug.isNotEmpty()) " ($debug)" else "",
+                )
+                .joinToString("\n")
+
+    private var strategy: String = "NONE"
+    private var debug: String = ""
+
+    override fun update(sim: Simulator, dt: Float) {
+        if (!enabled) return
+
+        if (sim.now < nextStrategyTime) {
+            return
+        }
+
+        val currentStrategy = strategy
+
+        if (ship.landing != null) {
+            if (target != null) {
+                strategy = "LANDED"
+                debug = ""
+                // we just got here. see the sights.
+                target = null
+                landingAltitude = 0f
+                nextStrategyTime = sim.now + SIGHTSEEING_TIME
+            } else {
+                // full power until we blast off
+                ship.thrust = Vec2.makeWithAngleMag(ship.angle, 1f)
+
+                strategy = "LAUNCHING"
+                debug = ""
+                nextStrategyTime = sim.now + 2f
+            }
+        } else {
+            // select new target
+
+            if (target == null) {
+                // testing: target the first planet
+                //   target = universe.planets[0]
+
+                // target the nearest unexplored planet
+                target =
+                    universe.planets
+                        .sortedBy { (it.pos - ship.pos).mag() }
+                        .firstOrNull { !it.explored }
+                brakingDistance = 0f
+
+                // if we've explored them all, pick one at random
+                if (target == null) target = universe.planets.random()
+            }
+
+            target?.let { target -> // should be nonnull
+                val shipV = ship.velocity
+                val targetV = target.velocity
+                val targetVector = (target.pos - ship.pos)
+                val altitude = targetVector.mag() - target.radius
+
+                landingAltitude = min(target.radius, 100f)
+
+                // the following is in the moving reference frame of the target
+                val relativeV: Vec2 = shipV - targetV
+                val projection = relativeV.dot(targetVector / targetVector.mag())
+                val relativeSpeed = relativeV.mag() * projection.sign
+                val timeToTarget = if (relativeSpeed != 0f) altitude / relativeSpeed else 1_000f
+
+                val newBrakingDistance =
+                    BRAKING_TIME * if (relativeSpeed > 0) relativeSpeed else MAIN_ENGINE_ACCEL
+                brakingDistance =
+                    expSmooth(brakingDistance, newBrakingDistance, dt = sim.dt, speed = 5f)
+
+                // We're going to aim at where the target will be, but we want to make sure to
+                // compute
+                leadingPos =
+                    target.pos +
+                        Vec2.makeWithAngleMag(
+                            target.velocity.angle(),
+                            min(altitude / 2, target.velocity.mag())
+                        )
+                leadingVector = leadingPos - ship.pos
+
+                if (altitude < landingAltitude) {
+                    strategy = "LANDING"
+                    // Strategy: zero thrust, face away, prepare for landing
+
+                    ship.angle = (ship.pos - target.pos).angle() // point away from ground
+                    ship.thrust = Vec2.Zero
+                } else {
+                    if (relativeSpeed < 0 || altitude > brakingDistance) {
+                        strategy = "CHASING"
+                        // Strategy: Make tracks. We are either a long way away, or falling behind.
+                        ship.angle = leadingVector.angle()
+
+                        ship.thrust = Vec2.makeWithAngleMag(ship.angle, 1.0f)
+                    } else {
+                        strategy = "APPROACHING"
+                        // Strategy: Just slow down. If we get caught in the gravity well, it will
+                        // gradually start pulling us more in the direction of the planet, which
+                        // will create a graceful deceleration
+                        ship.angle = (-ship.velocity).angle()
+
+                        // We want to bleed off velocity over time. Specifically, relativeSpeed px/s
+                        // over timeToTarget seconds.
+                        val decel = relativeSpeed / timeToTarget
+                        val decelThrust =
+                            decel / MAIN_ENGINE_ACCEL * 0.9f // not quite slowing down enough
+                        ship.thrust = Vec2.makeWithAngleMag(ship.angle, decelThrust)
+                    }
+                }
+                debug = ("DV=%.0f D=%.0f T%+.1f").format(relativeSpeed, altitude, timeToTarget)
+            }
+            if (strategy != currentStrategy) {
+                nextStrategyTime = sim.now + STRATEGY_MIN_TIME
+            }
+        }
+    }
+
+    override fun postUpdate(sim: Simulator, dt: Float) {
+        if (!enabled) return
+    }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Colors.kt b/packages/EasterEgg/src/com/android/egg/landroid/Colors.kt
index f5657ae..24c4975 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Colors.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Colors.kt
@@ -19,11 +19,22 @@
 import androidx.compose.ui.graphics.Color
 
 /** Various UI colors. */
-object Colors {
-    val Eigengrau = Color(0xFF16161D)
-    val Eigengrau2 = Color(0xFF292936)
-    val Eigengrau3 = Color(0xFF3C3C4F)
-    val Eigengrau4 = Color(0xFFA7A7CA)
+class Colors {
+    object Android {
+        val Green = Color(0xFF34A853)
+        val Blue = Color(0xFF4285F4)
+        val Mint = Color(0xFFE8F5E9)
+        val Chartreuse = Color(0xFFC6FF00)
+    }
+    companion object {
+        val Eigengrau = Color(0xFF16161D)
+        val Eigengrau2 = Color(0xFF292936)
+        val Eigengrau3 = Color(0xFF3C3C4F)
+        val Eigengrau4 = Color(0xFFA7A7CA)
 
-    val Console = Color(0xFFB7B7FF)
+        val Console = Color(0xFFB7B7FF)
+        val Autopilot = Android.Blue
+        val Track = Android.Green
+        val Flag = Android.Chartreuse
+    }
 }
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt
new file mode 100644
index 0000000..8c87c5d
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.egg.landroid
+
+import android.service.dreams.DreamService
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleRegistry
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.savedstate.SavedStateRegistryController
+import androidx.savedstate.SavedStateRegistryOwner
+import androidx.savedstate.setViewTreeSavedStateRegistryOwner
+import androidx.window.layout.FoldingFeature
+import kotlin.random.Random
+
+class DreamUniverse : DreamService() {
+    private var foldState = mutableStateOf<FoldingFeature?>(null) // unused
+
+    private val lifecycleOwner =
+        object : SavedStateRegistryOwner {
+            override val lifecycle = LifecycleRegistry(this)
+            override val savedStateRegistry
+                get() = savedStateRegistryController.savedStateRegistry
+
+            private val savedStateRegistryController =
+                SavedStateRegistryController.create(this).apply { performAttach() }
+
+            fun onCreate() {
+                savedStateRegistryController.performRestore(null)
+                lifecycle.currentState = Lifecycle.State.CREATED
+            }
+
+            fun onStart() {
+                lifecycle.currentState = Lifecycle.State.STARTED
+            }
+
+            fun onStop() {
+                lifecycle.currentState = Lifecycle.State.CREATED
+            }
+        }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+
+        val universe = VisibleUniverse(namer = Namer(resources), randomSeed = randomSeed())
+
+        isInteractive = false
+
+        if (TEST_UNIVERSE) {
+            universe.initTest()
+        } else {
+            universe.initRandom()
+
+            // We actually don't want the deterministic random position of the ship, we want
+            // true randomness to keep things interesting. So use Random (not universe.rng).
+            universe.ship.pos =
+                universe.star.pos +
+                    Vec2.makeWithAngleMag(
+                        Random.nextFloat() * PI2f,
+                        Random.nextFloatInRange(
+                            PLANET_ORBIT_RANGE.start,
+                            PLANET_ORBIT_RANGE.endInclusive
+                        )
+                    )
+        }
+
+        // enable autopilot in screensaver mode
+        val autopilot = Autopilot(universe.ship, universe)
+        universe.ship.autopilot = autopilot
+        universe.add(autopilot)
+        autopilot.enabled = true
+
+        // much more visually interesting in a screensaver context
+        DYNAMIC_ZOOM = true
+
+        val composeView = ComposeView(this)
+        composeView.setContent {
+            Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState)
+            DebugText(DEBUG_TEXT)
+            Telemetry(universe)
+        }
+
+        composeView.setViewTreeLifecycleOwner(lifecycleOwner)
+        composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner)
+
+        setContentView(composeView)
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+        lifecycleOwner.onCreate()
+    }
+
+    override fun onDreamingStarted() {
+        super.onDreamingStarted()
+        lifecycleOwner.onStart()
+    }
+
+    override fun onDreamingStopped() {
+        super.onDreamingStopped()
+        lifecycleOwner.onStop()
+    }
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
index 5a9b814..79f8b5fc 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
@@ -21,12 +21,10 @@
 import android.util.Log
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
 import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.CubicBezierEasing
 import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.tween
 import androidx.compose.animation.core.withInfiniteAnimationFrameNanos
-import androidx.compose.animation.fadeIn
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.border
 import androidx.compose.foundation.gestures.awaitFirstDown
@@ -34,12 +32,14 @@
 import androidx.compose.foundation.gestures.rememberTransformableState
 import androidx.compose.foundation.gestures.transformable
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeContent
+import androidx.compose.foundation.layout.windowInsetsPadding
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
@@ -49,6 +49,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.AbsoluteAlignment.Left
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.geometry.Offset
@@ -59,8 +60,10 @@
 import androidx.compose.ui.graphics.drawscope.translate
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.toUpperCase
 import androidx.compose.ui.tooling.preview.Devices
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
@@ -94,12 +97,12 @@
 val RANDOM_SEED_TYPE = RandomSeedType.Daily
 
 const val FIXED_RANDOM_SEED = 5038L
-const val DEFAULT_CAMERA_ZOOM = 0.25f
+const val DEFAULT_CAMERA_ZOOM = 1f
 const val MIN_CAMERA_ZOOM = 250f / UNIVERSE_RANGE // 0.0025f
 const val MAX_CAMERA_ZOOM = 5f
-const val TOUCH_CAMERA_PAN = false
-const val TOUCH_CAMERA_ZOOM = true
-const val DYNAMIC_ZOOM = false // @@@ FIXME
+var TOUCH_CAMERA_PAN = false
+var TOUCH_CAMERA_ZOOM = false
+var DYNAMIC_ZOOM = false
 
 fun dailySeed(): Long {
     val today = GregorianCalendar()
@@ -134,39 +137,21 @@
 }
 
 @Composable
-fun ColumnScope.ConsoleText(
-    modifier: Modifier = Modifier,
-    visible: Boolean = true,
-    random: Random = Random.Default,
-    text: String
-) {
-    AnimatedVisibility(
-        modifier = modifier,
-        visible = visible,
-        enter =
-            fadeIn(
-                animationSpec =
-                    tween(
-                        durationMillis = 1000,
-                        easing = flickerFadeEasing(random) * CubicBezierEasing(0f, 1f, 1f, 0f)
-                    )
-            )
-    ) {
-        Text(
-            fontFamily = FontFamily.Monospace,
-            fontWeight = FontWeight.Medium,
-            fontSize = 12.sp,
-            color = Color(0xFFFF8000),
-            text = text
-        )
-    }
-}
-
-@Composable
 fun Telemetry(universe: VisibleUniverse) {
     var topVisible by remember { mutableStateOf(false) }
     var bottomVisible by remember { mutableStateOf(false) }
 
+    var catalogFontSize by remember { mutableStateOf(9.sp) }
+
+    val textStyle =
+        TextStyle(
+            fontFamily = FontFamily.Monospace,
+            fontWeight = FontWeight.Medium,
+            fontSize = 12.sp,
+            letterSpacing = 1.sp,
+            lineHeight = 12.sp,
+        )
+
     LaunchedEffect("blah") {
         delay(1000)
         bottomVisible = true
@@ -174,65 +159,109 @@
         topVisible = true
     }
 
-    Column(modifier = Modifier.fillMaxSize().padding(6.dp)) {
-        universe.triggerDraw.value // recompose on every frame
-        val explored = universe.planets.filter { it.explored }
+    universe.triggerDraw.value // recompose on every frame
 
-        AnimatedVisibility(modifier = Modifier, visible = topVisible, enter = flickerFadeIn) {
-            Text(
-                fontFamily = FontFamily.Monospace,
-                fontWeight = FontWeight.Medium,
-                fontSize = 12.sp,
-                color = Colors.Console,
-                modifier = Modifier.align(Left),
-                text =
-                    with(universe.star) {
-                        "  STAR: $name (UDC-${universe.randomSeed % 100_000})\n" +
-                            " CLASS: ${cls.name}\n" +
-                            "RADIUS: ${radius.toInt()}\n" +
-                            "  MASS: %.3g\n".format(mass) +
-                            "BODIES: ${explored.size} / ${universe.planets.size}\n" +
-                            "\n"
-                    } +
-                        explored
-                            .map {
-                                "  BODY: ${it.name}\n" +
-                                    "  TYPE: ${it.description.capitalize()}\n" +
-                                    "  ATMO: ${it.atmosphere.capitalize()}\n" +
-                                    " FAUNA: ${it.fauna.capitalize()}\n" +
-                                    " FLORA: ${it.flora.capitalize()}\n"
-                            }
-                            .joinToString("\n")
+    val explored = universe.planets.filter { it.explored }
 
-                // TODO: different colors, highlight latest discovery
+    BoxWithConstraints(
+        modifier =
+            Modifier.fillMaxSize().padding(6.dp).windowInsetsPadding(WindowInsets.safeContent),
+    ) {
+        val wide = maxWidth > maxHeight
+        Column(
+            modifier =
+                Modifier.align(if (wide) Alignment.BottomEnd else Alignment.BottomStart)
+                    .fillMaxWidth(if (wide) 0.45f else 1.0f)
+        ) {
+            universe.ship.autopilot?.let { autopilot ->
+                if (autopilot.enabled) {
+                    AnimatedVisibility(
+                        modifier = Modifier,
+                        visible = bottomVisible,
+                        enter = flickerFadeIn
+                    ) {
+                        Text(
+                            style = textStyle,
+                            color = Colors.Autopilot,
+                            modifier = Modifier.align(Left),
+                            text = autopilot.telemetry
+                        )
+                    }
+                }
+            }
+
+            AnimatedVisibility(
+                modifier = Modifier,
+                visible = bottomVisible,
+                enter = flickerFadeIn
+            ) {
+                Text(
+                    style = textStyle,
+                    color = Colors.Console,
+                    modifier = Modifier.align(Left),
+                    text =
+                        with(universe.ship) {
+                            val closest = universe.closestPlanet()
+                            val distToClosest = ((closest.pos - pos).mag() - closest.radius).toInt()
+                            listOfNotNull(
+                                    landing?.let {
+                                        "LND: ${it.planet.name.toUpperCase()}\nJOB: ${it.text}"
+                                    }
+                                        ?: if (distToClosest < 10_000) {
+                                            "ALT: $distToClosest"
+                                        } else null,
+                                    "THR: %.0f%%".format(thrust.mag() * 100f),
+                                    "POS: %s".format(pos.str("%+7.0f")),
+                                    "VEL: %.0f".format(velocity.mag())
+                                )
+                                .joinToString("\n")
+                        }
                 )
+            }
         }
 
-        Spacer(modifier = Modifier.weight(1f))
-
-        AnimatedVisibility(modifier = Modifier, visible = bottomVisible, enter = flickerFadeIn) {
+        AnimatedVisibility(
+            modifier = Modifier.align(Alignment.TopStart),
+            visible = topVisible,
+            enter = flickerFadeIn
+        ) {
             Text(
-                fontFamily = FontFamily.Monospace,
-                fontWeight = FontWeight.Medium,
-                fontSize = 12.sp,
+                style = textStyle,
+                fontSize = catalogFontSize,
+                lineHeight = catalogFontSize,
+                letterSpacing = 1.sp,
                 color = Colors.Console,
-                modifier = Modifier.align(Left),
-                text =
-                    with(universe.ship) {
-                        val closest = universe.closestPlanet()
-                        val distToClosest = (closest.pos - pos).mag().toInt()
-                        listOfNotNull(
-                                landing?.let { "LND: ${it.planet.name}" }
-                                    ?: if (distToClosest < 10_000) {
-                                        "ALT: $distToClosest"
-                                    } else null,
-                                if (thrust != Vec2.Zero) "THR: %.0f%%".format(thrust.mag() * 100f)
-                                else null,
-                                "POS: %s".format(pos.str("%+7.0f")),
-                                "VEL: %.0f".format(velocity.mag())
-                            )
-                            .joinToString("\n")
+                onTextLayout = { textLayoutResult ->
+                    if (textLayoutResult.didOverflowHeight) {
+                        catalogFontSize = 8.sp
                     }
+                },
+                text =
+                    (with(universe.star) {
+                            listOf(
+                                "  STAR: $name (UDC-${universe.randomSeed % 100_000})",
+                                " CLASS: ${cls.name}",
+                                "RADIUS: ${radius.toInt()}",
+                                "  MASS: %.3g".format(mass),
+                                "BODIES: ${explored.size} / ${universe.planets.size}",
+                                ""
+                            )
+                        } +
+                            explored
+                                .map {
+                                    listOf(
+                                        "  BODY: ${it.name}",
+                                        "  TYPE: ${it.description.capitalize()}",
+                                        "  ATMO: ${it.atmosphere.capitalize()}",
+                                        " FAUNA: ${it.fauna.capitalize()}",
+                                        " FLORA: ${it.flora.capitalize()}",
+                                        ""
+                                    )
+                                }
+                                .flatten())
+                        .joinToString("\n")
+
+                // TODO: different colors, highlight latest discovery
             )
         }
     }
@@ -246,6 +275,8 @@
 
         onWindowLayoutInfoChange()
 
+        enableEdgeToEdge()
+
         val universe = VisibleUniverse(namer = Namer(resources), randomSeed = randomSeed())
 
         if (TEST_UNIVERSE) {
@@ -254,6 +285,15 @@
             universe.initRandom()
         }
 
+        com.android.egg.ComponentActivationActivity.lockUnlockComponents(applicationContext)
+
+        // for autopilot testing in the activity
+        //        val autopilot = Autopilot(universe.ship, universe)
+        //        universe.ship.autopilot = autopilot
+        //        universe.add(autopilot)
+        //        autopilot.enabled = true
+        //        DYNAMIC_ZOOM = autopilot.enabled
+
         setContent {
             Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState)
             DebugText(DEBUG_TEXT)
@@ -437,8 +477,13 @@
         val distToNearestSurf = max(0f, (u.ship.pos - closest.pos).mag() - closest.radius * 1.2f)
         //        val normalizedDist = clamp(distToNearestSurf, 50f, 50_000f) / 50_000f
         if (DYNAMIC_ZOOM) {
-            //            cameraZoom = lerp(0.1f, 5f, smooth(1f-normalizedDist))
-            cameraZoom = clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM)
+            cameraZoom =
+                expSmooth(
+                    cameraZoom,
+                    clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM),
+                    dt = u.dt,
+                    speed = 1.5f
+                )
         } else if (!TOUCH_CAMERA_ZOOM) cameraZoom = DEFAULT_CAMERA_ZOOM
         if (!TOUCH_CAMERA_PAN) cameraOffset = (u.follow?.pos ?: Vec2.Zero) * -1f
 
@@ -478,26 +523,26 @@
                 "star: '${u.star.name}' designation=UDC-${u.randomSeed % 100_000} " +
                 "class=${u.star.cls.name} r=${u.star.radius.toInt()} m=${u.star.mass}\n" +
                 "planets: ${u.planets.size}\n" +
-                    u.planets.joinToString("\n") {
-                        val range = (u.ship.pos - it.pos).mag()
-                        val vorbit = sqrt(GRAVITATION * it.mass / range)
-                        val vescape = sqrt(2 * GRAVITATION * it.mass / it.radius)
-                        " * ${it.name}:\n" +
-                                if (it.explored) {
-                                    "   TYPE:  ${it.description.capitalize()}\n" +
-                                            "   ATMO:  ${it.atmosphere.capitalize()}\n" +
-                                            "   FAUNA: ${it.fauna.capitalize()}\n" +
-                                            "   FLORA: ${it.flora.capitalize()}\n"
-                                } else {
-                                    "   (Unexplored)\n"
-                                } +
-                                "   orbit=${(it.pos - it.orbitCenter).mag().toInt()}" +
-                                " radius=${it.radius.toInt()}" +
-                                " mass=${"%g".format(it.mass)}" +
-                                " vel=${(it.speed).toInt()}" +
-                                " // range=${"%.0f".format(range)}" +
-                                " vorbit=${vorbit.toInt()} vescape=${vescape.toInt()}"
-                    })
+                u.planets.joinToString("\n") {
+                    val range = (u.ship.pos - it.pos).mag()
+                    val vorbit = sqrt(GRAVITATION * it.mass / range)
+                    val vescape = sqrt(2 * GRAVITATION * it.mass / it.radius)
+                    " * ${it.name}:\n" +
+                        if (it.explored) {
+                            "   TYPE:  ${it.description.capitalize()}\n" +
+                                "   ATMO:  ${it.atmosphere.capitalize()}\n" +
+                                "   FAUNA: ${it.fauna.capitalize()}\n" +
+                                "   FLORA: ${it.flora.capitalize()}\n"
+                        } else {
+                            "   (Unexplored)\n"
+                        } +
+                        "   orbit=${(it.pos - it.orbitCenter).mag().toInt()}" +
+                        " radius=${it.radius.toInt()}" +
+                        " mass=${"%g".format(it.mass)}" +
+                        " vel=${(it.speed).toInt()}" +
+                        " // range=${"%.0f".format(range)}" +
+                        " vorbit=${vorbit.toInt()} vescape=${vescape.toInt()}"
+                })
 
         zoom(cameraZoom) {
             // All coordinates are space coordinates now.
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Maths.kt b/packages/EasterEgg/src/com/android/egg/landroid/Maths.kt
index fdf29f7..a1e8212 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Maths.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Maths.kt
@@ -16,6 +16,7 @@
 
 package com.android.egg.landroid
 
+import kotlin.math.exp
 import kotlin.math.pow
 
 /** smoothstep. Ken Perlin's version */
@@ -32,3 +33,8 @@
 fun lexp(start: Float, end: Float, progress: Float): Float {
     return (progress - start) / (end - start)
 }
+
+/** Exponentially smooth current toward target by a factor of speed. */
+fun expSmooth(current: Float, target: Float, dt: Float = 1f / 60, speed: Float = 5f): Float {
+    return current + (target - current) * (1 - exp(-dt * speed))
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
index 67d536e..7331807 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
@@ -17,9 +17,8 @@
 package com.android.egg.landroid
 
 import android.content.res.Resources
-import kotlin.random.Random
-
 import com.android.egg.R
+import kotlin.random.Random
 
 const val SUFFIX_PROB = 0.75f
 const val LETTER_PROB = 0.3f
@@ -62,6 +61,11 @@
             0.1f to "(^*!%@##!!"
         )
 
+    private var activities = Bag(resources.getStringArray(R.array.activities))
+    private var floraGenericPlurals = Bag(resources.getStringArray(R.array.flora_generic_plurals))
+    private var faunaGenericPlurals = Bag(resources.getStringArray(R.array.fauna_generic_plurals))
+    private var atmoGenericPlurals = Bag(resources.getStringArray(R.array.atmo_generic_plurals))
+
     fun describePlanet(rng: Random): String {
         return planetTable.roll(rng).pull(rng) + " " + planetTypes.pull(rng)
     }
@@ -93,4 +97,30 @@
     fun describeAtmo(rng: Random): String {
         return atmoTable.roll(rng).pull(rng)
     }
+
+    fun floraPlural(rng: Random): String {
+        return floraGenericPlurals.pull(rng)
+    }
+    fun faunaPlural(rng: Random): String {
+        return faunaGenericPlurals.pull(rng)
+    }
+    fun atmoPlural(rng: Random): String {
+        return atmoGenericPlurals.pull(rng)
+    }
+
+    val TEMPLATE_REGEX = Regex("""\{(flora|fauna|planet|atmo)\}""")
+    fun describeActivity(rng: Random, target: Planet?): String {
+        return activities
+            .pull(rng)
+            .replace(TEMPLATE_REGEX) {
+                when (it.groupValues[1]) {
+                    "flora" -> (target?.flora ?: "SOME") + " " + floraPlural(rng)
+                    "fauna" -> (target?.fauna ?: "SOME") + " " + faunaPlural(rng)
+                    "atmo" -> (target?.atmosphere ?: "SOME") + " " + atmoPlural(rng)
+                    "planet" -> (target?.description ?: "SOME BODY") // once told me
+                    else -> "unknown template tag: ${it.groupValues[0]}"
+                }
+            }
+            .toUpperCase()
+    }
 }
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt b/packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt
index 8510640..cd87335 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt
@@ -32,6 +32,13 @@
     }
 }
 
+fun createPolygonPoints(radius: Float, sides: Int): List<Vec2> {
+    val angleStep = PI2f / sides
+    return (0 until sides).map { i ->
+        Vec2(radius * cos(angleStep * i), radius * sin(angleStep * i))
+    }
+}
+
 fun createStar(radius1: Float, radius2: Float, points: Int): Path {
     return Path().apply {
         val angleStep = PI2f / points
@@ -46,15 +53,16 @@
 }
 
 fun Path.parseSvgPathData(d: String) {
-    Regex("([A-Z])([-.,0-9e ]+)").findAll(d.trim()).forEach {
+    Regex("([A-Za-z])\\s*([-.,0-9e ]+)").findAll(d.trim()).forEach {
         val cmd = it.groups[1]!!.value
         val args =
             it.groups[2]?.value?.split(Regex("\\s+"))?.map { v -> v.toFloat() } ?: emptyList()
-        Log.d("Landroid", "cmd = $cmd, args = " + args.joinToString(","))
+        // Log.d("Landroid", "cmd = $cmd, args = " + args.joinToString(","))
         when (cmd) {
             "M" -> moveTo(args[0], args[1])
             "C" -> cubicTo(args[0], args[1], args[2], args[3], args[4], args[5])
             "L" -> lineTo(args[0], args[1])
+            "l" -> relativeLineTo(args[0], args[1])
             "Z" -> close()
             else -> Log.v("Landroid", "unsupported SVG command: $cmd")
         }
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Randomness.kt b/packages/EasterEgg/src/com/android/egg/landroid/Randomness.kt
index ebbb2bd..2903534 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Randomness.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Randomness.kt
@@ -61,6 +61,7 @@
 /** Return a random float in the range [start, end). */
 fun Random.nextFloatInRange(fromUntil: ClosedFloatingPointRange<Float>): Float =
     nextFloatInRange(fromUntil.start, fromUntil.endInclusive)
+
 /** Return a random float in the range [first, second). */
 fun Random.nextFloatInRange(fromUntil: Pair<Float, Float>): Float =
     nextFloatInRange(fromUntil.first, fromUntil.second)
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Universe.kt b/packages/EasterEgg/src/com/android/egg/landroid/Universe.kt
index 11dce61..1e54569 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Universe.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Universe.kt
@@ -155,10 +155,7 @@
                     speed = speed,
                     color = Colors.Eigengrau4
                 )
-            android.util.Log.v(
-                "Landroid",
-                "created planet $p with period $period and vel $speed"
-            )
+            android.util.Log.v("Landroid", "created planet $p with period $period and vel $speed")
             val num = it + 1
             p.description = "TEST PLANET #$num"
             p.atmosphere = "radius=$radius"
@@ -215,10 +212,7 @@
                     speed = speed,
                     color = Colors.Eigengrau4
                 )
-            android.util.Log.v(
-                "Landroid",
-                "created planet $p with period $period and vel $speed"
-            )
+            android.util.Log.v("Landroid", "created planet $p with period $period and vel $speed")
             p.description = namer.describePlanet(rng)
             p.atmosphere = namer.describeAtmo(rng)
             p.flora = namer.describeLife(rng)
@@ -302,7 +296,7 @@
                     //                        &&
                     //                        vDiff < 100f
                     ) {
-                        val landing = Landing(ship, planet, a)
+                        val landing = Landing(ship, planet, a, namer.describeActivity(rng, planet))
                         ship.landing = landing
                         ship.velocity = planet.velocity
                         add(landing)
@@ -370,12 +364,15 @@
     }
 }
 
-class Landing(val ship: Spacecraft, val planet: Planet, val angle: Float) : Constraint {
-    private val landingVector = Vec2.makeWithAngleMag(angle, ship.radius + planet.radius)
+class Landing(var ship: Spacecraft?, val planet: Planet, val angle: Float, val text: String = "") :
+    Constraint {
     override fun solve(sim: Simulator, dt: Float) {
-        val desiredPos = planet.pos + landingVector
-        ship.pos = (ship.pos * 0.5f) + (desiredPos * 0.5f) // @@@ FIXME
-        ship.angle = angle
+        ship?.let { ship ->
+            val landingVector = Vec2.makeWithAngleMag(angle, ship.radius + planet.radius)
+            val desiredPos = planet.pos + landingVector
+            ship.pos = (ship.pos * 0.5f) + (desiredPos * 0.5f) // @@@ FIXME
+            ship.angle = angle
+        }
     }
 }
 
@@ -435,6 +432,7 @@
     val track = Track()
 
     var landing: Landing? = null
+    var autopilot: Autopilot? = null
 
     init {
         mass = SPACECRAFT_MASS
@@ -448,23 +446,19 @@
             var deltaV = MAIN_ENGINE_ACCEL * dt
             if (SCALED_THRUST) deltaV *= thrustMag.coerceIn(0f, 1f)
 
-            if (landing == null) {
-                // we are free in space, so we attempt to pivot toward the desired direction
-                // NOTE: no longer required thanks to FlightStick
-                // angle = thrust.angle()
-            } else
-                landing?.let { landing ->
-                    if (launchClock == 0f) launchClock = sim.now + 1f /* @@@ TODO extract */
+            // check if we are currently attached to a landing
+            landing?.let { landing ->
+                // launch clock is 1 second long
+                if (launchClock == 0f) launchClock = sim.now + 1f /* @@@ TODO extract */
 
-                    if (sim.now > launchClock) {
-                        // first-stage to orbit has 1000x power
-                        //                    deltaV *= 1000f
-                        sim.remove(landing)
-                        this.landing = null
-                    } else {
-                        deltaV = 0f
-                    }
+                if (sim.now > launchClock) {
+                    // detach from landing site
+                    landing.ship = null
+                    this.landing = null
+                } else {
+                    deltaV = 0f
                 }
+            }
 
             // this is it. impart thrust to the ship.
             // note that we always thrust in the forward direction
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/VisibleUniverse.kt b/packages/EasterEgg/src/com/android/egg/landroid/VisibleUniverse.kt
index 6baf36e..974784d 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/VisibleUniverse.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/VisibleUniverse.kt
@@ -28,11 +28,10 @@
 import androidx.compose.ui.graphics.drawscope.translate
 import androidx.compose.ui.util.lerp
 import androidx.core.math.MathUtils.clamp
+import com.android.egg.flags.Flags.flagFlag
 import java.lang.Float.max
 import kotlin.math.sqrt
 
-import com.android.egg.flags.Flags.flagFlag
-
 const val DRAW_ORBITS = true
 const val DRAW_GRAVITATIONAL_FIELDS = true
 const val DRAW_STAR_GRAVITATIONAL_FIELDS = true
@@ -71,16 +70,6 @@
     with(universe) {
         triggerDraw.value // Please recompose when this value changes.
 
-        //        star.drawZoomed(ds, zoom)
-        //        planets.forEach { p ->
-        //            p.drawZoomed(ds, zoom)
-        //            if (p == follow) {
-        //                drawCircle(Color.Red, 20f / zoom, p.pos)
-        //            }
-        //        }
-        //
-        //        ship.drawZoomed(ds, zoom)
-
         constraints.forEach {
             when (it) {
                 is Landing -> drawLanding(it)
@@ -89,13 +78,14 @@
         }
         drawStar(star)
         entities.forEach {
-            if (it === ship || it === star) return@forEach // draw the ship last
+            if (it === star) return@forEach // don't draw the star as a planet
             when (it) {
-                is Spacecraft -> drawSpacecraft(it)
                 is Spark -> drawSpark(it)
                 is Planet -> drawPlanet(it)
+                else -> Unit // draw these at a different time, or not at all
             }
         }
+        ship.autopilot?.let { drawAutopilot(it) }
         drawSpacecraft(ship)
     }
 }
@@ -111,15 +101,6 @@
                 pathEffect = PathEffect.dashPathEffect(floatArrayOf(8f / zoom, 8f / zoom), 0f)
             )
     )
-    //    val path = Path().apply {
-    //        fillType = PathFillType.EvenOdd
-    //        addOval(Rect(center = Vec2.Zero, radius = container.radius))
-    //        addOval(Rect(center = Vec2.Zero, radius = container.radius + 10_000))
-    //    }
-    //    drawPath(
-    //        path = path,
-    //
-    //    )
 }
 
 fun ZoomedDrawScope.drawGravitationalField(planet: Planet) {
@@ -226,23 +207,47 @@
 """
         )
     }
-val thrustPath = createPolygon(-3f, 3).also { it.translate(Vec2(-4f, 0f)) }
+val spaceshipLegs =
+    Path().apply {
+        parseSvgPathData(
+            """
+M-7   -6.5
+l-3.5  0
+l-1   -2
+l 0    4
+l 1   -2
+Z
+M-7    6.5
+l-3.5  0
+l-1   -2
+l 0    4
+l 1   -2
+Z
+"""
+        )
+    }
+val thrustPath = createPolygon(-3f, 3).also { it.translate(Vec2(-5f, 0f)) }
 
 fun ZoomedDrawScope.drawSpacecraft(ship: Spacecraft) {
     with(ship) {
         rotateRad(angle, pivot = pos) {
             translate(pos.x, pos.y) {
-                //                drawPath(
-                //                    path = createStar(200f, 100f, 3),
-                //                    color = Color.White,
-                //                    style = Stroke(width = 2f / zoom)
-                //                )
+                // new in V: little landing legs
+                ship.landing?.let {
+                    drawPath(
+                        path = spaceshipLegs,
+                        color = Color(0xFFCCCCCC),
+                        style = Stroke(width = 2f / this@drawSpacecraft.zoom)
+                    )
+                }
+                // draw the ship
                 drawPath(path = spaceshipPath, color = Colors.Eigengrau) // fauxpaque
                 drawPath(
                     path = spaceshipPath,
                     color = if (transit) Color.Black else Color.White,
                     style = Stroke(width = 2f / this@drawSpacecraft.zoom)
                 )
+                // draw thrust
                 if (thrust != Vec2.Zero) {
                     drawPath(
                         path = thrustPath,
@@ -254,27 +259,8 @@
                             )
                     )
                 }
-                //                drawRect(
-                //                    topLeft = Offset(-1f, -1f),
-                //                    size = Size(2f, 2f),
-                //                    color = Color.Cyan,
-                //                    style = Stroke(width = 2f / zoom)
-                //                )
-                //                drawLine(
-                //                    start = Vec2.Zero,
-                //                    end = Vec2(20f, 0f),
-                //                    color = Color.Cyan,
-                //                    strokeWidth = 2f / zoom
-                //                )
             }
         }
-        //        // DEBUG: draw velocity vector
-        //        drawLine(
-        //            start = pos,
-        //            end = pos + velocity,
-        //            color = Color.Red,
-        //            strokeWidth = 3f / zoom
-        //        )
         drawTrack(track)
     }
 }
@@ -287,14 +273,15 @@
         val height = 80f
         rotateRad(landing.angle, pivot = v) {
             translate(v.x, v.y) {
-                drawPath(
+                val flagPath =
                     Path().apply {
                         moveTo(0f, 0f)
                         lineTo(height, 0f)
                         lineTo(height * 0.875f, height * 0.25f)
                         lineTo(height * 0.75f, 0f)
                         close()
-                    }, Color.Yellow, style = Stroke(width = strokeWidth))
+                    }
+                drawPath(flagPath, Colors.Flag, style = Stroke(width = strokeWidth))
             }
         }
     }
@@ -311,10 +298,7 @@
             Spark.Style.DOT -> drawCircle(color, size, pos)
             Spark.Style.DOT_ABSOLUTE -> drawCircle(color, size, pos / zoom)
             Spark.Style.RING -> drawCircle(color, size, pos, style = Stroke(width = 1f / zoom))
-        //                drawPoints(listOf(pos), PointMode.Points, color, strokeWidth = 2f/zoom)
-        //            drawCircle(color, 2f/zoom, pos)
         }
-        //        drawCircle(Color.Gray, center = pos, radius = 1.5f / zoom)
     }
 }
 
@@ -324,19 +308,9 @@
             drawPoints(
                 positions,
                 pointMode = PointMode.Lines,
-                color = Color.Green,
+                color = Colors.Track,
                 strokeWidth = 1f / zoom
             )
-            //            if (positions.size < 2) return
-            //            drawPath(Path()
-            //                .apply {
-            //                    val p = positions[positions.size - 1]
-            //                    moveTo(p.x, p.y)
-            //                    positions.reversed().subList(1, positions.size).forEach { p ->
-            //                        lineTo(p.x, p.y)
-            //                    }
-            //                },
-            //                color = Color.Green, style = Stroke(1f/zoom))
         } else {
             if (positions.size < 2) return
             var prev: Vec2 = positions[positions.size - 1]
@@ -349,3 +323,43 @@
         }
     }
 }
+
+fun ZoomedDrawScope.drawAutopilot(autopilot: Autopilot) {
+    val color = Colors.Autopilot.copy(alpha = 0.5f)
+
+    autopilot.target?.let { target ->
+        val zoom = zoom
+        rotateRad(autopilot.universe.now * PI2f / 10f, target.pos) {
+            translate(target.pos.x, target.pos.y) {
+                drawPath(
+                    path =
+                        createPolygon(
+                            radius = target.radius + autopilot.brakingDistance,
+                            sides = 15 // Autopilot introduced in Android 15
+                        ),
+                    color = color,
+                    style = Stroke(1f / zoom)
+                )
+                drawCircle(
+                    color,
+                    radius = target.radius + autopilot.landingAltitude / 2,
+                    center = Vec2.Zero,
+                    alpha = 0.25f,
+                    style = Stroke(autopilot.landingAltitude)
+                )
+            }
+        }
+        drawLine(
+            color,
+            start = autopilot.ship.pos,
+            end = autopilot.leadingPos,
+            strokeWidth = 1f / zoom
+        )
+        drawCircle(
+            color,
+            radius = 5f / zoom,
+            center = autopilot.leadingPos,
+            style = Stroke(1f / zoom)
+        )
+    }
+}