Use typed tactics modules for converted panels

ober

0f6d34f48ce25605092ef91fac5be70dfc603e33

diff --git a/templates/original-tactics-parts/SfcShipArtView.ss b/templates/original-tactics-parts/SfcShipArtView.ss
index c24d584..05da16c 100644
--- a/templates/original-tactics-parts/SfcShipArtView.ss
+++ b/templates/original-tactics-parts/SfcShipArtView.ss
@@ -7,6 +7,7 @@
                       (android graphics Canvas)
                       (android graphics Color)
                       (android graphics Paint)
+                      (android graphics Paint Style)
                       (android graphics Rect)
                       (android graphics RectF)
                       (android view View)
@@ -21,20 +22,20 @@
         (type Int32)
         (type JSONObject)
         (type Paint)
-        (type PaintStyle)
+        (type Style)
         (type Rect)
         (type RectF)
         (type Resources)
         (type View)
         (extern (paintAntiAliasFlag) : Int32
           (kotlin-value Paint ANTI_ALIAS_FLAG))
-        (extern (paintStyleFill) : PaintStyle
+        (extern (paintStyleFill) : Style
           (kotlin-value Paint Style FILL))
-        (extern (paintStyleStroke) : PaintStyle
+        (extern (paintStyleStroke) : Style
           (kotlin-value Paint Style STROKE))
         (extern (paintColorSet (paint : Paint) (color : Int32)) : Unit
           (kotlin-member-set color))
-        (extern (paintStyleSet (paint : Paint) (style : PaintStyle)) : Unit
+        (extern (paintStyleSet (paint : Paint) (style : Style)) : Unit
           (kotlin-member-set style))
         (extern (paintStrokeWidthSet
                   (paint : Paint)
@@ -135,9 +136,6 @@
           (kotlin-member-get height))
         (extern (float32ToInt (value : Float32)) : Int32
           (kotlin-member-call toInt))
-        (extern (gameBattleState
-                  (game : JSONObject)) : (Nullable JSONObject)
-          (kotlin-call gameBattleState))
         (extern (jsonObjectChild
                   (json : JSONObject)
                   (key : String)) : (Nullable JSONObject)
@@ -156,7 +154,7 @@
         (def (configureShipArtPaint
                (paint : Paint)
                (color : Int32)
-               (style : PaintStyle)
+               (style : Style)
                (strokeWidth : Float32)) : Paint
           (begin
             (paintColorSet paint color)
@@ -278,7 +276,7 @@
               (let ((battle
                       (if (nullable-null? game)
                         (nullable-none JSONObject)
-                        (gameBattleState (nullable-get game)))))
+                        (jsonObjectChild (nullable-get game) "battle-state"))))
                 (if (nullable-null? battle)
                   (begin
                     (canvasDrawText
diff --git a/templates/original-tactics.ss b/templates/original-tactics.ss
index fd90f77..d438b54 100644
--- a/templates/original-tactics.ss
+++ b/templates/original-tactics.ss
@@ -1,11253 +1,7113 @@
 (import (jerboa prelude))
 
-;; Generated from app/src/main/java by Codex while migrating the Android app to jerboa-android.
-;; The Android project consumes this .ss fragment instead of Kotlin source files.
-
 (def fragment
-  '(
-    (kotlin-file-lines "com/jerboa/originaltactics/BattleCommandView.kt"
-      ("package com.jerboa.originaltactics"
-       ""
-       "import android.content.Context"
-       "import android.graphics.Canvas"
-       "import android.graphics.Color"
-       "import android.graphics.Paint"
-       "import android.graphics.RectF"
-       "import android.view.View"
-       "import org.json.JSONArray"
-       "import org.json.JSONObject"
-       "import kotlin.math.max"
-       "import kotlin.math.min"
-       ""
-       "class BattleCommandView(context: Context) : View(context) {"
-       "    private var game: JSONObject? = null"
-       "    private var events: JSONArray = JSONArray()"
-       "    private var tacticalStatus: JSONObject? = null"
-       "    private var drawWidth = 0f"
-       "    private var drawHeight = 0f"
-       ""
-       "    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(23, 29, 39)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val panelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(34, 42, 55)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(83, 98, 122)"
-       "        style = Paint.Style.STROKE"
-       "        strokeWidth = 2f"
-       "    }"
-       "    private val alphaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(47, 128, 222)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val betaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(215, 92, 72)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(58, 68, 84)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(70, 167, 106)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val accentPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(230, 155, 63)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val titlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.WHITE"
-       "        textSize = 27f"
-       "        isFakeBoldText = true"
-       "    }"
-       "    private val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(213, 221, 234)"
-       "        textSize = 20f"
-       "    }"
-       "    private val smallPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(165, 177, 195)"
-       "        textSize = 17f"
-       "    }"
-       "    private val valuePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.WHITE"
-       "        textSize = 19f"
-       "        isFakeBoldText = true"
-       "    }"
-       ""
-       "    fun setGame(nextGame: JSONObject?, nextEvents: JSONArray = JSONArray(), nextStatus: JSONObject? = tacticalStatus) {"
-       "        game = nextGame"
-       "        events = nextEvents"
-       "        tacticalStatus = nextStatus"
-       "        invalidate()"
-       "    }"
-       ""
-       "    fun setTacticalStatus(nextStatus: JSONObject?) {"
-       "        tacticalStatus = nextStatus"
-       "        invalidate()"
-       "    }"
-       ""
-       "    override fun onDraw(canvas: Canvas) {"
-       "        super.onDraw(canvas)"
-       "        val canvasState = canvas.save()"
-       "        val scale = resources.displayMetrics.density.coerceAtLeast(1f)"
-       "        drawWidth = width.toFloat() / scale"
-       "        drawHeight = height.toFloat() / scale"
-       "        canvas.scale(scale, scale)"
-       "        try {"
-       "            drawContent(canvas)"
-       "        } finally {"
-       "            canvas.restoreToCount(canvasState)"
-       "        }"
-       "    }"
-       ""
-       "    private fun drawContent(canvas: Canvas) {"
-       "        canvas.drawRoundRect(RectF(0f, 0f, drawWidth, drawHeight), 10f, 10f, backgroundPaint)"
-       ""
-       "        val current = game"
-       "        val battle = current?.optJSONObject(\"battle-state\")"
-       "        if (current == null || battle == null) {"
-       "            drawEmpty(canvas)"
-       "            return"
-       "        }"
-       ""
-       "        drawHeader(canvas, current, battle)"
-       "        drawOrderStrip(canvas, current, battle)"
-       "        val orders = current.optJSONObject(\"orders\")"
-       "        val currentImpulse = current.optInt(\"impulse\", 0)"
-       ""
-       "        val gap = 10f"
-       "        val top = 82f"
-       "        val feedHeight = min(54f, drawHeight * 0.32f)"
-       "        val cardsBottom = max(top + 76f, drawHeight - feedHeight - 8f)"
-       "        val cardWidth = (drawWidth - gap * 3f) / 2f"
-       "        drawShipCard("
-       "            canvas,"
-       "            RectF(gap, top, gap + cardWidth, cardsBottom),"
-       "            \"ALPHA\","
-       "            battle.optJSONObject(\"alpha\"),"
-       "            battle.optJSONObject(\"alpha-position\"),"
-       "            tacticalStatus?.optJSONObject(\"alpha\"),"
-       "            orders,"
-       "            \"alpha\","
-       "            currentImpulse,"
-       "            alphaPaint"
-       "        )"
-       "        drawShipCard("
-       "            canvas,"
-       "            RectF(gap * 2f + cardWidth, top, gap * 2f + cardWidth * 2f, cardsBottom),"
-       "            \"BETA\","
-       "            battle.optJSONObject(\"beta\"),"
-       "            battle.optJSONObject(\"beta-position\"),"
-       "            tacticalStatus?.optJSONObject(\"beta\"),"
-       "            orders,"
-       "            \"beta\","
-       "            currentImpulse,"
-       "            betaPaint"
-       "        )"
-       "        drawEventFeed(canvas, RectF(gap, cardsBottom + 6f, drawWidth - gap, drawHeight - 8f))"
-       "    }"
-       ""
-       "    private fun drawEmpty(canvas: Canvas) {"
-       "        titlePaint.textAlign = Paint.Align.CENTER"
-       "        canvas.drawText(\"Battle command\", drawWidth / 2f, drawHeight / 2f - 2f, titlePaint)"
-       "        smallPaint.textAlign = Paint.Align.CENTER"
-       "        canvas.drawText(\"Start an engagement\", drawWidth / 2f, drawHeight / 2f + 24f, smallPaint)"
-       "    }"
-       ""
-       "    private fun drawHeader(canvas: Canvas, current: JSONObject, battle: JSONObject) {"
-       "        val turn = current.optInt(\"turn\", 1)"
-       "        val impulse = current.optInt(\"impulse\", 0)"
-       "        val range = currentBattleRange(battle)"
-       "        val winner = current.optString(\"winner\", \"undecided\")"
-       "        val fired = current.optBoolean(\"fired?\", false)"
-       "        val fireState = if (fired) \"FIRE RESOLVED\" else \"FIRE READY\""
-       "        val outcome = if (winner == \"undecided\") \"NO DECISION\" else \"${winner.uppercase()} WINS\""
-       ""
-       "        titlePaint.textAlign = Paint.Align.LEFT"
-       "        canvas.drawText(\"T$turn.$impulse\", 12f, 29f, titlePaint)"
-       "        valuePaint.textAlign = Paint.Align.CENTER"
-       "        canvas.drawText(\"RANGE $range\", drawWidth / 2f, 29f, valuePaint)"
-       "        val statusText = \"$fireState  $outcome\""
-       "        valuePaint.textAlign = Paint.Align.RIGHT"
-       "        canvas.drawText(fitText(statusText, valuePaint, drawWidth * 0.44f), drawWidth - 12f, 29f, valuePaint)"
-       "    }"
-       ""
-       "    private fun drawOrderStrip(canvas: Canvas, current: JSONObject, battle: JSONObject) {"
-       "        val orders = current.optJSONObject(\"orders\") ?: return"
-       "        val currentImpulse = current.optInt(\"impulse\", 0)"
-       "        val fireImpulse = orders.optInt(\"fire-impulse\", 32)"
-       "        val alpha = maneuverPlotText(orders.opt(\"alpha-maneuvers\"), currentImpulse, 3)"
-       "        val beta = maneuverPlotText(orders.opt(\"beta-maneuvers\"), currentImpulse, 3)"
-       "        val ordersText = \"FIRE I$fireImpulse    A $alpha    B $beta\""
-       "        val forecastText = rangeForecastLine(battle, orders, currentImpulse)"
-       ""
-       "        val rect = RectF(10f, 39f, drawWidth - 10f, 74f)"
-       "        canvas.drawRoundRect(rect, 8f, 8f, panelPaint)"
-       "        canvas.drawRoundRect(rect, 8f, 8f, borderPaint)"
-       "        if (forecastText.isBlank()) {"
-       "            valuePaint.textAlign = Paint.Align.CENTER"
-       "            canvas.drawText(fitText(ordersText, valuePaint, rect.width() - 18f), rect.centerX(), rect.top + 24f, valuePaint)"
-       "        } else {"
-       "            valuePaint.textAlign = Paint.Align.LEFT"
-       "            canvas.drawText(fitText(ordersText, valuePaint, rect.width() - 18f), rect.left + 9f, rect.top + 16f, valuePaint)"
-       "            smallPaint.textAlign = Paint.Align.LEFT"
-       "            canvas.drawText(fitText(forecastText, smallPaint, rect.width() - 18f), rect.left + 9f, rect.top + 32f, smallPaint)"
-       "        }"
-       "    }"
-       ""
-       "    private fun drawShipCard("
-       "        canvas: Canvas,"
-       "        rect: RectF,"
-       "        side: String,"
-       "        ship: JSONObject?,"
-       "        position: JSONObject?,"
-       "        statusActor: JSONObject?,"
-       "        orders: JSONObject?,"
-       "        actor: String,"
-       "        currentImpulse: Int,"
-       "        sidePaint: Paint"
-       "    ) {"
-       "        canvas.drawRoundRect(rect, 9f, 9f, panelPaint)"
-       "        canvas.drawRoundRect(rect, 9f, 9f, borderPaint)"
-       "        canvas.drawRoundRect(RectF(rect.left, rect.top, rect.right, rect.top + 28f), 9f, 9f, sidePaint)"
-       ""
-       "        valuePaint.textAlign = Paint.Align.LEFT"
-       "        canvas.drawText(side, rect.left + 9f, rect.top + 20f, valuePaint)"
-       ""
-       "        val name = shipDisplayLabel(ship)"
-       "        labelPaint.textAlign = Paint.Align.LEFT"
-       "        canvas.drawText(fitText(name, labelPaint, rect.width() - 18f), rect.left + 9f, rect.top + 50f, labelPaint)"
-       ""
-       "        val speed = position?.optInt(\"speed\", 0) ?: 0"
-       "        val facing = position?.optInt(\"facing\", 0) ?: 0"
-       "        val helm = maneuverPlotText(orders?.opt(\"$actor-maneuvers\"), currentImpulse, 3)"
-       "        val nextMove = nextMoveImpulse(position, currentImpulse)?.let { \"M I$it\" } ?: \"HOLD\""
-       "        smallPaint.textAlign = Paint.Align.LEFT"
-       "        canvas.drawText(fitText(\"SPD $speed   F$facing   $nextMove   HELM $helm\", smallPaint, rect.width() - 18f), rect.left + 9f, rect.top + 70f, smallPaint)"
-       ""
-       "        val internalNow = ship?.optInt(\"remaining-internals\", 0) ?: 0"
-       "        val internalMax = maxInternals(ship, internalNow)"
-       "        drawMetricBar(canvas, rect.left + 9f, rect.top + 84f, rect.width() - 18f, \"HULL\", internalNow, internalMax)"
-       ""
-       "        val shieldNow = shieldTotal(ship, false)"
-       "        val shieldMax = max(shieldTotal(ship, true), shieldNow)"
-       "        val paPanels = counterValue(ship?.opt(\"boxes\"), \"52\")"
-       "        val paMax = paPanelMax(ship)"
-       "        if (shieldMax <= 0 && (paPanels > 0 || paMax > 1)) {"
-       "            drawMetricBar(canvas, rect.left + 9f, rect.top + 116f, rect.width() - 18f, \"PA PANELS\", paPanels, paMax)"
-       "        } else {"
-       "            drawMetricBar(canvas, rect.left + 9f, rect.top + 116f, rect.width() - 18f, \"SHIELDS\", shieldNow, shieldMax)"
-       "        }"
-       ""
-       "        val weaponTotal = counterTotal(ship?.optJSONObject(\"weapons\"))"
-       "        val armedTotal = counterTotal(ship?.optJSONObject(\"armed-weapons\"))"
-       "        drawMetricBar(canvas, rect.left + 9f, rect.top + 148f, rect.width() - 18f, \"ARMED\", armedTotal, max(1, weaponTotal))"
-       ""
-       "        val fireSolution = actorFireSolution(statusActor)"
-       "        val fireLine = when {"
-       "            fireSolution.installed > 0 -> \"LOCK ${fireSolution.inArc}/${fireSolution.ready}   READY ${fireSolution.ready}/${fireSolution.armed}\""
-       "            else -> \"LOCK -   READY -\""
-       "        }"
-       "        smallPaint.textAlign = Paint.Align.LEFT"
-       "        canvas.drawText(fitText(fireLine, smallPaint, rect.width() - 18f), rect.left + 9f, min(rect.bottom - 30f, rect.top + 180f), smallPaint)"
-       ""
-       "        val cool = counterTotal(ship?.optJSONObject(\"direct-fire-cooldown\"))"
-       "        val launch = counterTotal(ship?.optJSONObject(\"seeking-launch-limits\"))"
-       "        val families = weaponFamilyLine(statusActor)"
-       "        val primary = primaryWeaponLine(statusActor)"
-       "        val footer = when {"
-       "            families.isNotBlank() -> \"$families   COOL $cool\""
-       "            primary.isNotBlank() -> \"$primary   COOL $cool\""
-       "            else -> \"COOL $cool   LAUNCH $launch\""
-       "        }"
-       "        canvas.drawText(fitText(footer, smallPaint, rect.width() - 18f), rect.left + 9f, min(rect.bottom - 10f, rect.top + 201f), smallPaint)"
-       "    }"
-       ""
-       "    private fun drawMetricBar(canvas: Canvas, x: Float, y: Float, barWidth: Float, label: String, value: Int, maxValue: Int) {"
-       "        val safeMax = max(1, maxValue)"
-       "        val ratio = (value.toFloat() / safeMax.toFloat()).coerceIn(0f, 1f)"
-       "        labelPaint.textAlign = Paint.Align.LEFT"
-       "        canvas.drawText(label, x, y + 13f, smallPaint)"
-       "        valuePaint.textAlign = Paint.Align.RIGHT"
-       "        canvas.drawText(\"$value/$safeMax\", x + barWidth, y + 13f, valuePaint)"
-       ""
-       "        val top = y + 18f"
-       "        val rect = RectF(x, top, x + barWidth, top + 9f)"
-       "        canvas.drawRoundRect(rect, 5f, 5f, trackPaint)"
-       "        if (ratio > 0f) {"
-       "            fillPaint.color = barColor(ratio)"
-       "            canvas.drawRoundRect(RectF(rect.left, rect.top, rect.left + rect.width() * ratio, rect.bottom), 5f, 5f, fillPaint)"
-       "        }"
-       "    }"
-       ""
-       "    private fun drawEventFeed(canvas: Canvas, rect: RectF) {"
-       "        canvas.drawRoundRect(rect, 8f, 8f, panelPaint)"
-       "        canvas.drawRoundRect(rect, 8f, 8f, borderPaint)"
-       "        smallPaint.textAlign = Paint.Align.LEFT"
-       "        smallPaint.color = Color.rgb(165, 177, 195)"
-       "        canvas.drawText(\"TACTICAL LOG\", rect.left + 9f, rect.top + 18f, smallPaint)"
-       ""
-       "        val lines = recentEventLines(3)"
-       "        if (lines.isEmpty()) {"
-       "            labelPaint.textAlign = Paint.Align.LEFT"
-       "            canvas.drawText(\"No combat events yet\", rect.left + 9f, rect.top + 42f, labelPaint)"
-       "        } else {"
-       "            var y = rect.top + 40f"
-       "            lines.forEachIndexed { index, line ->"
-       "                if (y > rect.bottom - 6f) return@forEachIndexed"
-       "                if (index == lines.lastIndex) {"
-       "                    canvas.drawCircle(rect.left + 10f, y - 5f, 4f, accentPaint)"
-       "                }"
-       "                labelPaint.textAlign = Paint.Align.LEFT"
-       "                canvas.drawText(fitText(line, labelPaint, rect.width() - 28f), rect.left + 20f, y, labelPaint)"
-       "                y += 19f"
-       "            }"
-       "        }"
-       "        smallPaint.color = Color.rgb(165, 177, 195)"
-       "    }"
-       ""
-       "    private fun recentEventLines(limit: Int): List<String> {"
-       "        val out = mutableListOf<String>()"
-       "        for (index in events.length() - 1 downTo 0) {"
-       "            val event = events.optJSONObject(index) ?: continue"
-       "            val type = event.optString(\"type\", \"\")"
-       "            if (type.isBlank() || type == \"impulse-activity\") continue"
-       "            out.add(0, summarizeEvent(event))"
-       "            if (out.size >= limit) break"
-       "        }"
-       "        return out"
-       "    }"
-       ""
-       "    private fun summarizeEvent(event: JSONObject): String {"
-       "        val turn = event.optInt(\"turn\", -1)"
-       "        val impulse = event.optInt(\"impulse\", -1)"
-       "        val actor = event.optString(\"actor\", \"\")"
-       "        val type = event.optString(\"type\", \"event\")"
-       "        val payload = event.optJSONObject(\"payload\")"
-       "        val prefix = when {"
-       "            turn >= 0 && impulse >= 0 -> \"T$turn.$impulse\""
-       "            impulse >= 0 -> \"I$impulse\""
-       "            else -> \"LOG\""
-       "        }"
-       "        val actorText = if (actor.isBlank()) \"\" else \" ${actor.uppercase()}\""
-       "        val detail = when (type) {"
-       "            \"fire-weapons\" -> fireSummary(payload)"
-       "            \"seeking-launch\" -> launchSummary(payload)"
-       "            \"seeking-impact\" -> impactSummary(payload)"
-       "            \"movement\" -> movementSummary(payload)"
-       "            \"turn\", \"sideslip\", \"high-energy-turn\" -> movementSummary(payload)"
-       "            \"turn-blocked\", \"maneuver-blocked\", \"fire-control-blocked\" -> blockedSummary(payload)"
-       "            \"ew-control\", \"ew-support\", \"ew-support-rejected\", \"ew-support-lost\","
-       "            \"lock-on-control\", \"shield-control\", \"energy-allocation\", \"wild-swac\" -> controlSummary(payload)"
-       "            \"web-created\", \"web-reduced\", \"web-caster\", \"web-snare\", \"web-breaker\" -> webSummary(payload)"
-       "            \"turn-completed\" -> \"turn complete\""
-       "            \"orders-submitted\" -> \"orders committed\""
-       "            else -> payload?.optString(\"reason\", type)?.takeIf { it.isNotBlank() } ?: type"
-       "        }"
-       "        return \"$prefix$actorText $detail\""
-       "    }"
-       ""
-       "    private fun fireSummary(payload: JSONObject?): String {"
-       "        if (payload == null) return \"weapons fire\""
-       "        val summaries = payload.optJSONArray(\"fire-target-summaries\")"
-       "        if (summaries != null && summaries.length() > 0) {"
-       "            val parts = mutableListOf<String>()"
-       "            for (index in 0 until summaries.length()) {"
-       "                val summary = summaries.optJSONObject(index) ?: continue"
-       "                val attacker = sideLabel(summary.optString(\"attacker\", \"\"))"
-       "                val target = sideLabel(summary.optString(\"target\", \"\"))"
-       "                val damage = numberValue(summary.opt(\"damage\"))"
-       "                val internals = numberValue(summary.opt(\"internals\"))"
-       "                val face = shieldFaceLabel(summary.opt(\"shield-facing\"))"
-       "                val before = numberValue(summary.opt(\"shield-before\"))"
-       "                val after = numberValue(summary.opt(\"shield-after\"))"
-       "                if (damage <= 0 && internals <= 0) continue"
-       "                val shield = if (face.isBlank()) \"\" else \" $face $before>$after\""
-       "                val internalText = if (internals > 0) \" int $internals\" else \"\""
-       "                parts.add(\"$attacker>$target dmg $damage$shield$internalText\")"
-       "                if (parts.size >= 2) break"
-       "            }"
-       "            if (parts.isNotEmpty()) return parts.joinToString(\"  \")"
-       "        }"
-       "        val alphaDamage = payload.optJSONObject(\"alpha-volley\")?.optInt(\"damage\", 0) ?: 0"
-       "        val betaDamage = payload.optJSONObject(\"beta-volley\")?.optInt(\"damage\", 0) ?: 0"
-       "        val range = payload.opt(\"range-before\") ?: payload.opt(\"range\") ?: \"-\""
-       "        return \"weapons fire R$range A:$alphaDamage B:$betaDamage\""
-       "    }"
-       ""
-       "    private fun launchSummary(payload: JSONObject?): String {"
-       "        val seeker = payload?.optJSONObject(\"seeker\") ?: return \"seeker launch\""
-       "        val kind = seeker.optString(\"kind\", \"seeker\")"
-       "        val id = seeker.opt(\"id\") ?: seeker.opt(\"seeking-id\") ?: \"?\""
-       "        val target = seeker.optString(\"target\", \"?\")"
-       "        return \"launch $kind #$id to $target\""
-       "    }"
-       ""
-       "    private fun impactSummary(payload: JSONObject?): String {"
-       "        if (payload == null) return \"seeker impact\""
-       "        val kind = payload.optString(\"kind\", payload.optString(\"seeker-kind\", \"seeker\"))"
-       "        val target = payload.optString(\"target\", \"?\")"
-       "        val damage = payload.opt(\"damage\") ?: \"-\""
-       "        val face = shieldFaceLabel(payload.opt(\"shield-facing\"))"
-       "        val before = numberValue(payload.opt(\"shield-before\"))"
-       "        val after = numberValue(payload.opt(\"shield-after\"))"
-       "        val shield = if (face.isBlank()) \"\" else \" $face $before>$after\""
-       "        return \"$kind impact $damage on $target$shield\""
-       "    }"
-       ""
-       "    private fun movementSummary(payload: JSONObject?): String {"
-       "        val after = payload?.optJSONObject(\"position-after\") ?: return \"maneuver\""
-       "        return \"move q${after.optInt(\"q\")} r${after.optInt(\"r\")} f${after.optInt(\"facing\")}\""
-       "    }"
-       ""
-       "    private fun blockedSummary(payload: JSONObject?): String {"
-       "        if (payload == null) return \"blocked\""
-       "        val maneuver = payload.optString(\"maneuver\", \"\").uppercase()"
-       "        val reason = payload.optString(\"reason\", \"blocked\").replace('-', ' ')"
-       "        return listOf(maneuver, reason).filter { it.isNotBlank() }.joinToString(\" \")"
-       "    }"
-       ""
-       "    private fun controlSummary(payload: JSONObject?): String ="
-       "        payloadFieldSummary("
-       "            payload,"
-       "            listOf(\"mode\", \"reason\", \"ecm\", \"eccm\", \"ew-shift\", \"power\", \"reserve-power\", \"fire-control-active?\", \"shield\", \"target\")"
-       "        ).ifBlank { \"control\" }"
-       ""
-       "    private fun webSummary(payload: JSONObject?): String ="
-       "        payloadFieldSummary(payload, listOf(\"strength\", \"web-strength\", \"web-hexes\", \"hexes\", \"damage\", \"range\", \"reason\"))"
-       "            .ifBlank { \"web action\" }"
-       ""
-       "    private fun payloadFieldSummary(payload: JSONObject?, keys: List<String>): String {"
-       "        if (payload == null) return \"\""
-       "        val parts = mutableListOf<String>()"
-       "        keys.forEach { key ->"
-       "            if (!payload.has(key)) return@forEach"
-       "            val value = payload.opt(key)"
-       "            if (value == null || value == JSONObject.NULL) return@forEach"
-       "            parts.add(\"${fieldLabel(key)} ${compactPayloadValue(value)}\")"
-       "        }"
-       "        return parts.take(3).joinToString(\" \")"
-       "    }"
-       ""
-       "    private fun fieldLabel(key: String): String ="
-       "        key.removeSuffix(\"?\")"
-       "            .replace(\"-active\", \"\")"
-       "            .replace(\"shield-facing\", \"shield\")"
-       "            .replace('-', ' ')"
-       "            .uppercase()"
-       ""
-       "    private fun compactPayloadValue(value: Any?): String ="
-       "        when (value) {"
-       "            is JSONObject -> {"
-       "                val q = value.opt(\"q\")"
-       "                val r = value.opt(\"r\")"
-       "                val facing = value.opt(\"facing\")"
-       "                if (q != null && r != null) \"q$q/r$r/f${facing ?: \"-\"}\" else \"{${value.length()}}\""
-       "            }"
-       "            is JSONArray -> value.length().toString()"
-       "            is Boolean -> if (value) \"Y\" else \"N\""
-       "            else -> value.toString().replace('-', ' ')"
-       "        }"
-       ""
-       "    private fun maneuverPlotText(raw: Any?, currentImpulse: Int, limit: Int): String {"
-       "        val entries = maneuverEntries(raw)"
-       "            .filter { it.first > currentImpulse && it.second.isNotBlank() }"
-       "            .sortedBy { it.first }"
-       "            .take(limit)"
-       "        if (entries.isEmpty()) return \"CLEAR\""
-       "        return entries.joinToString(\"/\") { (impulse, maneuver) ->"
-       "            \"I$impulse ${maneuverAbbrev(maneuver)}\""
-       "        }"
-       "    }"
-       ""
-       "    private fun rangeForecastLine(battle: JSONObject, orders: JSONObject, currentImpulse: Int): String {"
-       "        val forecast = projectedRangeForecast("
-       "            battle.optJSONObject(\"alpha-position\"),"
-       "            battle.optJSONObject(\"beta-position\"),"
-       "            orders.opt(\"alpha-maneuvers\"),"
-       "            orders.opt(\"beta-maneuvers\"),"
-       "            currentImpulse,"
-       "            maxMoves = 8,"
-       "            labelLimit = 5"
-       "        )"
-       "        if (forecast.isEmpty()) return \"\""
-       "        val currentRange = hexRangeFromPositions("
-       "            battle.optJSONObject(\"alpha-position\"),"
-       "            battle.optJSONObject(\"beta-position\")"
-       "        )"
-       "        val finalRange = forecast.last().range"
-       "        val trend = when {"
-       "            currentRange == null -> \"FORECAST\""
-       "            finalRange < currentRange -> \"CLOSING\""
-       "            finalRange > currentRange -> \"OPENING\""
-       "            else -> \"RANGE\""
-       "        }"
-       "        val points = forecast.take(4).joinToString(\"  \") { \"I${it.impulse} R${it.range}\" }"
-       "        return \"$trend $points\""
-       "    }"
-       ""
-       "    private fun maneuverEntries(raw: Any?): List<Pair<Int, String>> {"
-       "        val entries = mutableListOf<Pair<Int, String>>()"
-       "        when (raw) {"
-       "            is JSONArray -> {"
-       "                for (index in 0 until raw.length()) {"
-       "                    val item = raw.opt(index)"
-       "                    when (item) {"
-       "                        is JSONObject -> {"
-       "                            val pair = item.optJSONArray(\"\\$pair\")"
-       "                            if (pair != null) {"
-       "                                val impulse = pair.optInt(0, -1)"
-       "                                val maneuver = pair.optString(1, \"\")"
-       "                                if (impulse > 0 && maneuver.isNotBlank()) entries.add(impulse to maneuver)"
-       "                            }"
-       "                        }"
-       "                        is JSONArray -> {"
-       "                            val impulse = item.optInt(0, -1)"
-       "                            val maneuver = item.optString(1, \"\")"
-       "                            if (impulse > 0 && maneuver.isNotBlank()) entries.add(impulse to maneuver)"
-       "                        }"
-       "                    }"
-       "                }"
-       "            }"
-       "            is JSONObject -> {"
-       "                val keys = raw.keys()"
-       "                while (keys.hasNext()) {"
-       "                    val key = keys.next()"
-       "                    val impulse = key.toIntOrNull() ?: continue"
-       "                    val maneuver = raw.optString(key, \"\")"
-       "                    if (impulse > 0 && maneuver.isNotBlank()) entries.add(impulse to maneuver)"
-       "                }"
-       "            }"
-       "        }"
-       "        return entries"
-       "    }"
-       ""
-       "    private fun maneuverAbbrev(maneuver: String): String ="
-       "        maneuverAbbreviation(maneuver)"
-       ""
-       "    private fun maxInternals(ship: JSONObject?, current: Int): Int {"
-       "        val explicit = ship?.optInt(\"max-internals\", 0) ?: 0"
-       "        if (explicit > 0) return explicit"
-       "        return max(1, current + counterTotal(ship?.optJSONObject(\"destroyed-boxes\")))"
-       "    }"
-       ""
-       "    private fun shieldTotal(ship: JSONObject?, maxValues: Boolean): Int {"
-       "        val preferred = if (maxValues) ship?.optJSONObject(\"max-shields\") else ship?.optJSONObject(\"shields\")"
-       "        val fallback = ship?.optJSONObject(\"shields\")"
-       "        return counterTotal(preferred ?: fallback)"
-       "    }"
-       ""
-       "    private fun paPanelMax(ship: JSONObject?): Int {"
-       "        val current = counterValue(ship?.opt(\"boxes\"), \"52\")"
-       "        val destroyed = counterValue(ship?.opt(\"destroyed-boxes\"), \"52\")"
-       "        return max(1, current + destroyed)"
-       "    }"
-       ""
-       "    private fun counterTotal(counter: Any?): Int {"
-       "        var total = 0"
-       "        forEachCounterPair(counter) { _, value ->"
-       "            total += numberValue(value).coerceAtLeast(0)"
-       "        }"
-       "        return total"
-       "    }"
-       ""
-       "    private fun counterValue(counter: Any?, wantedKey: String): Int {"
-       "        var total = 0"
-       "        forEachCounterPair(counter) { key, value ->"
-       "            if (key == wantedKey) total += numberValue(value).coerceAtLeast(0)"
-       "        }"
-       "        return total"
-       "    }"
-       ""
-       "    private fun forEachCounterPair(counter: Any?, action: (String, Any?) -> Unit) {"
-       "        when (counter) {"
-       "            is JSONObject -> {"
-       "                val pair = counter.optJSONArray(PAIR_MARKER)"
-       "                if (pair != null) {"
-       "                    action(pair.opt(0)?.toString() ?: \"\", pair.opt(1))"
-       "                } else {"
-       "                    val keys = counter.keys()"
-       "                    while (keys.hasNext()) {"
-       "                        val key = keys.next()"
-       "                        action(key, counter.opt(key))"
-       "                    }"
-       "                }"
-       "            }"
-       "            is JSONArray -> {"
-       "                for (index in 0 until counter.length()) {"
-       "                    val pair = counter.optJSONObject(index)?.optJSONArray(PAIR_MARKER) ?: continue"
-       "                    action(pair.opt(0)?.toString() ?: \"\", pair.opt(1))"
-       "                }"
-       "            }"
-       "        }"
-       "    }"
-       ""
-       "    private fun numberValue(value: Any?): Int ="
-       "        when (value) {"
-       "            is Number -> value.toInt()"
-       "            is String -> value.toIntOrNull() ?: 0"
-       "            else -> 0"
-       "        }"
-       ""
-       "    private fun sideLabel(value: String): String ="
-       "        when (value.lowercase()) {"
-       "            \"alpha\" -> \"A\""
-       "            \"beta\" -> \"B\""
-       "            else -> value.uppercase().take(6)"
-       "        }"
-       ""
-       "    private fun shieldFaceLabel(value: Any?): String {"
-       "        val text = value?.toString() ?: return \"\""
-       "        if (text == \"#f\" || text == \"false\") return \"\""
-       "        if (text == \"all\") return \"ALL\""
-       "        val number = text.removePrefix(\"shield-\").toIntOrNull() ?: return text.uppercase()"
-       "        return \"SH$number\""
-       "    }"
-       ""
-       "    private fun actorFireSolution(actor: JSONObject?): FireSolution {"
-       "        val weapons = actor?.optJSONArray(\"weapon-status\") ?: return FireSolution()"
-       "        var installed = 0"
-       "        var armed = 0"
-       "        var ready = 0"
-       "        var inArc = 0"
-       "        for (index in 0 until weapons.length()) {"
-       "            val weapon = weapons.optJSONObject(index) ?: continue"
-       "            installed += weapon.optInt(\"installed\", 0).coerceAtLeast(0)"
-       "            armed += weapon.optInt(\"armed\", 0).coerceAtLeast(0)"
-       "            ready += weapon.optInt(\"fireable\", 0).coerceAtLeast(0)"
-       "            inArc += weapon.optInt(\"fireable-in-arc\", 0).coerceAtLeast(0)"
-       "        }"
-       "        return FireSolution(installed, armed, ready, inArc)"
-       "    }"
-       ""
-       "    private fun primaryWeaponLine(actor: JSONObject?): String {"
-       "        val weapons = actor?.optJSONArray(\"weapon-status\") ?: return \"\""
-       "        var best: JSONObject? = null"
-       "        for (index in 0 until weapons.length()) {"
-       "            val weapon = weapons.optJSONObject(index) ?: continue"
-       "            val current = best"
-       "            if (current == null || weaponScore(weapon) > weaponScore(current)) {"
-       "                best = weapon"
-       "            }"
-       "        }"
-       "        val weapon = best ?: return \"\""
-       "        val kind = shortWeapon(weapon.optString(\"kind\", \"-\"))"
-       "        val arc = compactArray(weapon.optJSONArray(\"arc-codes\"), 2)"
-       "        return \"$kind ${weapon.optInt(\"fireable-in-arc\", 0)}/${weapon.optInt(\"armed\", 0)}${if (arc.isBlank()) \"\" else \" $arc\"}\""
-       "    }"
-       ""
-       "    private fun weaponFamilyLine(actor: JSONObject?): String {"
-       "        val weapons = actor?.optJSONArray(\"weapon-status\") ?: return \"\""
-       "        val entries = mutableListOf<JSONObject>()"
-       "        for (index in 0 until weapons.length()) {"
-       "            val weapon = weapons.optJSONObject(index) ?: continue"
-       "            val installed = weapon.optInt(\"installed\", 0).coerceAtLeast(0)"
-       "            val armed = weapon.optInt(\"armed\", 0).coerceAtLeast(0)"
-       "            val fireable = weapon.optInt(\"fireable\", 0).coerceAtLeast(0)"
-       "            val cooldown = weapon.optInt(\"cooldown\", 0).coerceAtLeast(0)"
-       "            if (installed + armed + fireable + cooldown > 0) entries.add(weapon)"
-       "        }"
-       "        return entries"
-       "            .sortedWith(compareByDescending<JSONObject> { weaponScore(it) }.thenBy { it.optString(\"kind\", \"\") })"
-       "            .take(3)"
-       "            .joinToString(\"  \") { weapon ->"
-       "                val kind = shortWeapon(weapon.optString(\"kind\", \"-\"))"
-       "                val fireable = weapon.optInt(\"fireable-in-arc\", weapon.optInt(\"fireable\", 0)).coerceAtLeast(0)"
-       "                val armed = weapon.optInt(\"armed\", 0).coerceAtLeast(0)"
-       "                val installed = weapon.optInt(\"installed\", 0).coerceAtLeast(0)"
-       "                val cooldown = weapon.optInt(\"cooldown\", 0).coerceAtLeast(0)"
-       "                val cooldownText = if (cooldown > 0) \" cd$cooldown\" else \"\""
-       "                \"$kind $fireable/$armed/$installed$cooldownText\""
-       "            }"
-       "    }"
-       ""
-       "    private fun weaponScore(weapon: JSONObject): Int ="
-       "        weapon.optInt(\"fireable-in-arc\", 0) * 1000 +"
-       "            weapon.optInt(\"fireable\", 0) * 100 +"
-       "            weapon.optInt(\"armed\", 0) * 10 +"
-       "            weapon.optInt(\"installed\", 0)"
-       ""
-       "    private fun shortWeapon(kind: String): String ="
-       "        kind.replace(\"phaser-\", \"PH-\")"
-       "            .replace(\"photon\", \"PHOT\")"
-       "            .replace(\"disruptor\", \"DISR\")"
-       "            .replace(\"plasma-\", \"PL-\")"
-       "            .replace(\"drone-\", \"DR-\")"
-       "            .uppercase()"
-       ""
-       "    private fun compactArray(array: JSONArray?, limit: Int): String {"
-       "        if (array == null || array.length() == 0) return \"\""
-       "        val out = mutableListOf<String>()"
-       "        for (index in 0 until minOf(array.length(), limit)) {"
-       "            out.add(array.opt(index).toString())"
-       "        }"
-       "        if (array.length() > limit) out.add(\"+${array.length() - limit}\")"
-       "        return out.joinToString(\"/\")"
-       "    }"
-       ""
-       "    private fun barColor(ratio: Float): Int ="
-       "        when {"
-       "            ratio > 0.66f -> Color.rgb(70, 167, 106)"
-       "            ratio > 0.33f -> Color.rgb(230, 155, 63)"
-       "            else -> Color.rgb(215, 92, 72)"
-       "        }"
-       ""
-       "    private fun fitText(text: String, paint: Paint, maxWidth: Float): String {"
-       "        if (paint.measureText(text) <= maxWidth) return text"
-       "        var candidate = text"
-       "        while (candidate.length > 4 && paint.measureText(\"$candidate...\") > maxWidth) {"
-       "            candidate = candidate.dropLast(1)"
-       "        }"
-       "        return if (candidate.length <= 4) candidate else \"$candidate...\""
-       "    }"
-       ""
-       "    private data class FireSolution("
-       "        val installed: Int = 0,"
-       "        val armed: Int = 0,"
-       "        val ready: Int = 0,"
-       "        val inArc: Int = 0"
-       "    )"
-       ""
-       "    companion object {"
-       "        private const val PAIR_MARKER = \"$\" + \"pair\""
-       "    }"
-       "}"))
-    (kotlin-file-lines "com/jerboa/originaltactics/BridgeHudView.kt"
-      ("package com.jerboa.originaltactics"
-       ""
-       "import android.content.Context"
-       "import android.graphics.Canvas"
-       "import android.graphics.Color"
-       "import android.graphics.Paint"
-       "import android.graphics.RectF"
-       "import android.view.View"
-       "import org.json.JSONArray"
-       "import org.json.JSONObject"
-       "import kotlin.math.max"
-       ""
-       "class BridgeHudView(context: Context) : View(context) {"
-       "    private var game: JSONObject? = null"
-       "    private var dashboard: JSONObject? = null"
-       "    private var events: JSONArray = JSONArray()"
-       "    private var drawWidth = 0f"
-       "    private var drawHeight = 0f"
-       ""
-       "    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(8, 13, 21)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val panelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(18, 27, 40)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val panelAltPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(23, 34, 49)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(80, 102, 132)"
-       "        style = Paint.Style.STROKE"
-       "        strokeWidth = 2f"
-       "    }"
-       "    private val alphaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(54, 139, 232)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val betaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(217, 84, 69)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val readyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(76, 181, 114)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val warningPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(224, 161, 69)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val dangerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(191, 68, 62)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(50, 63, 83)"
-       "        style = Paint.Style.FILL"
-       "    }"
-       "    private val titlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.WHITE"
-       "        textSize = 25f"
-       "        isFakeBoldText = true"
-       "    }"
-       "    private val valuePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.WHITE"
-       "        textSize = 20f"
-       "        isFakeBoldText = true"
-       "    }"
-       "    private val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(202, 214, 232)"
-       "        textSize = 17f"
-       "    }"
-       "    private val smallPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(145, 162, 188)"
-       "        textSize = 15f"
-       "    }"
-       "    private val shieldTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.WHITE"
-       "        textSize = 12f"
-       "        isFakeBoldText = true"
-       "        textAlign = Paint.Align.CENTER"
-       "    }"
-       "    private val shieldHitPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = Color.rgb(255, 229, 120)"
-       "        style = Paint.Style.STROKE"
-       "        strokeWidth = 2.4f"
-       "    }"
-       ""
-       "    fun setData(nextGame: JSONObject?, nextDashboard: JSONObject?, nextEvents: JSONArray = JSONArray()) {"
-       "        game = nextGame"
-       "        dashboard = nextDashboard"
-       "        events = nextEvents"
-       "        invalidate()"
-       "    }"
-       ""
-       "    override fun onDraw(canvas: Canvas) {"
-       "        super.onDraw(canvas)"
-       "        val canvasState = canvas.save()"
-       "        val scale = resources.displayMetrics.density.coerceAtLeast(1f)"
-       "        drawWidth = width.toFloat() / scale"
-       "        drawHeight = height.toFloat() / scale"
-       "        canvas.scale(scale, scale)"
-       "        try {"
-       "            drawContent(canvas)"
-       "        } finally {"
-       "            canvas.restoreToCount(canvasState)"
-       "        }"
-       "    }"
-       ""
-       "    private fun drawContent(canvas: Canvas) {"
-       "        canvas.drawRect(0f, 0f, drawWidth, drawHeight, backgroundPaint)"
-       "        val current = game"
-       "        val battle = current?.optJSONObject(\"battle-state\")"
-       "        if (current == null || battle == null) {"
-       "            drawEmpty(canvas)"
-       "            return"
-       "        }"
-       ""
-       "        val status = dashboard?.optJSONObject(\"status\")"
-       "        drawHeader(canvas, current, battle, status)"
-       ""
-       "        val gap = 9f"
-       "        val top = 38f"
-       "        val bottom = drawHeight - 8f"
-       "        if (drawWidth < 520f) {"
-       "            val shipBottom = top + (bottom - top) * 0.62f"
-       "            val cardWidth = (drawWidth - gap * 3f) / 2f"
-       "            drawShipBlock("
-       "                canvas,"
-       "                RectF(gap, top, gap + cardWidth, shipBottom),"
-       "                \"ALPHA\","
-       "                battle.optJSONObject(\"alpha\"),"
-       "                status?.optJSONObject(\"alpha\"),"
-       "                shieldFacingIndex(battle.opt(\"alpha-shield-facing-hit\")),"
-       "                alphaPaint"
-       "            )"
-       "            drawShipBlock("
-       "                canvas,"
-       "                RectF(gap * 2f + cardWidth, top, gap * 2f + cardWidth * 2f, shipBottom),"
-       "                \"BETA\","
-       "                battle.optJSONObject(\"beta\"),"
-       "                status?.optJSONObject(\"beta\"),"
-       "                shieldFacingIndex(battle.opt(\"beta-shield-facing-hit\")),"
-       "                betaPaint"
-       "            )"
-       "            drawCenterBlock(canvas, RectF(gap, shipBottom + gap, drawWidth - gap, bottom), current, battle, status)"
-       "        } else {"
-       "            val leftWidth = drawWidth * 0.36f"
-       "            val rightWidth = drawWidth * 0.36f"
-       "            val centerLeft = gap + leftWidth + gap"
-       "            val centerRight = drawWidth - gap - rightWidth - gap"
-       "            drawShipBlock("
-       "                canvas,"
-       "                RectF(gap, top, gap + leftWidth, bottom),"
-       "                \"ALPHA\","
-       "                battle.optJSONObject(\"alpha\"),"
-       "                status?.optJSONObject(\"alpha\"),"
-       "                shieldFacingIndex(battle.opt(\"alpha-shield-facing-hit\")),"
-       "                alphaPaint"
-       "            )"
-       "            drawCenterBlock(canvas, RectF(centerLeft, top, centerRight, bottom), current, battle, status)"
-       "            drawShipBlock("
-       "                canvas,"
-       "                RectF(drawWidth - gap - rightWidth, top, drawWidth - gap, bottom),"
-       "                \"BETA\","
-       "                battle.optJSONObject(\"beta\"),"
-       "                status?.optJSONObject(\"beta\"),"
-       "                shieldFacingIndex(battle.opt(\"beta-shield-facing-hit\")),"
-       "                betaPaint"
-       "            )"
-       "        }"
-       "    }"
-       ""
-       "    private fun drawEmpty(canvas: Canvas) {"
-       "        titlePaint.textAlign = Paint.Align.CENTER"
-       "        canvas.drawText(\"TACTICAL COMMAND\", drawWidth / 2f, drawHeight / 2f - 5f, titlePaint)"
-       "        smallPaint.textAlign = Paint.Align.CENTER"
-       "        canvas.drawText(\"No active engagement\", drawWidth / 2f, drawHeight / 2f + 21f, smallPaint)"
-       "    }"
-       ""
-       "    private fun drawHeader(canvas: Canvas, current: JSONObject, battle: JSONObject, status: JSONObject?) {"
-       "        val turn = current.optInt(\"turn\", status?.optInt(\"turn\", 0) ?: 0)"
-       "        val impulse = current.optInt(\"impulse\", status?.optInt(\"impulse\", 0) ?: 0)"
-       "        val range = currentBattleRange(battle).ifBlank { (status?.opt(\"range\") ?: \"-\").toString() }"
-       "        val winner = current.optString(\"winner\", status?.optString(\"winner\", \"undecided\") ?: \"undecided\")"
-       "        val outcome = if (winner == \"undecided\") \"NO DECISION\" else \"${winner.uppercase()} WINS\""
-       ""
-       "        titlePaint.textAlign = Paint.Align.LEFT"
-       "        canvas.drawText(\"TACTICAL COMMAND\", 10f, 26f, titlePaint)"
-       "        valuePaint.textAlign = Paint.Align.CENTER"
-       "        canvas.drawText(\"T$turn.$impulse  RANGE $range\", drawWidth / 2f, 25f, valuePaint)"
-       "        valuePaint.textAlign = Paint.Align.RIGHT"
-       "        canvas.drawText(fitText(outcome, valuePaint, drawWidth * 0.34f), drawWidth - 10f, 25f, valuePaint)"
-       "    }"
-       ""
-       "    private fun drawShipBlock("
-       "        canvas: Canvas,"
-       "        rect: RectF,"
-       "        side: String,"
-       "        ship: JSONObject?,"
-       "        statusShip: JSONObject?,"
-       "        hitShield: Int,"
-       "        sidePaint: Paint"
-       "    ) {"
-       "        canvas.drawRoundRect(rect, 8f, 8f, panelPaint)"
-       "        canvas.drawRoundRect(rect, 8f, 8f, borderPaint)"
-       "        canvas.drawRoundRect(RectF(rect.left, rect.top, rect.right, rect.top + 25f), 8f, 8f, sidePaint)"
-       ""
-       "        valuePaint.textAlign = Paint.Align.LEFT"
-       "        canvas.drawText(side, rect.left + 8f, rect.top + 18f, valuePaint)"
-       "        smallPaint.textAlign = Paint.Align.RIGHT"
-       "        canvas.drawText(fitText(shipId(ship, statusShip), smallPaint, rect.width() * 0.46f), rect.right - 8f, rect.top + 17f, smallPaint)"
-       ""
-       "        var y = rect.top + 45f"