Use typed tactical map view

ober

341d5059ed0546f2646f8ca2f3a9bedb7cb35e78

diff --git a/templates/original-tactics.ss b/templates/original-tactics.ss
index 42f9104..004bd8e 100644
--- a/templates/original-tactics.ss
+++ b/templates/original-tactics.ss
@@ -3579,1859 +3579,7 @@
           "original-tactics-parts/SsdDamageView.kt.ss")
         (include-fragment
           "original-tactics-parts/SystemsReadinessView.kt.ss")
-        (kotlin-file-lines
-          "com/jerboa/originaltactics/TacticalMapView.kt"
-          ("package com.jerboa.originaltactics" ""
-           "import android.animation.ValueAnimator"
-           "import android.content.Context"
-           "import android.graphics.Canvas"
-           "import android.graphics.Color"
-           "import android.graphics.DashPathEffect"
-           "import android.graphics.Paint"
-           "import android.graphics.Path"
-           "import android.graphics.RectF"
-           "import android.os.SystemClock"
-           "import android.view.MotionEvent"
-           "import android.view.ScaleGestureDetector"
-           "import android.view.View"
-           "import android.view.animation.DecelerateInterpolator"
-           "import org.json.JSONArray" "import org.json.JSONObject"
-           "import kotlin.math.PI" "import kotlin.math.cos"
-           "import kotlin.math.max" "import kotlin.math.min"
-           "import kotlin.math.sin" "import kotlin.math.sqrt" ""
-           "class TacticalMapView(context: Context) : View(context) {"
-           "    private var game: JSONObject? = null"
-           "    private var events: JSONArray = JSONArray()"
-           "    private var tacticalStatus: JSONObject? = null"
-           "    private var tacticalDashboard: JSONObject? = null"
-           "    private var drawWidth = 0f"
-           "    private var drawHeight = 0f"
-           "    private var userScale = 1f"
-           "    private var userPanX = 0f"
-           "    private var userPanY = 0f"
-           "    private var lastTouchX = 0f"
-           "    private var lastTouchY = 0f"
-           "    private var movementProgress = 1f"
-           "    private var movementAnimator: ValueAnimator? = null"
-           "    private var alphaMoveStart: ShipPose? = null"
-           "    private var alphaMoveEnd: ShipPose? = null"
-           "    private var betaMoveStart: ShipPose? = null"
-           "    private var betaMoveEnd: ShipPose? = null"
-           "    private var impactStartedAt = 0L"
-           "    private var alphaImpactDamage = 0"
-           "    private var betaImpactDamage = 0"
-           "    private var alphaImpactShieldDamage = 0"
-           "    private var betaImpactShieldDamage = 0"
-           "    private var alphaImpactInternalDamage = 0"
-           "    private var betaImpactInternalDamage = 0"
-           "    private val scaleDetector = ScaleGestureDetector("
-           "        context,"
-           "        object : ScaleGestureDetector.SimpleOnScaleGestureListener() {"
-           "            override fun onScale(detector: ScaleGestureDetector): Boolean {"
-           "                userScale = (userScale * detector.scaleFactor).coerceIn(0.6f, 2.8f)"
-           "                invalidate()" "                return true"
-           "            }" "        }" "    )" ""
-           "    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(247, 249, 252)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(201, 210, 222)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 1.5f" "    }"
-           "    private val strongGridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(117, 131, 153)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 2.5f" "    }"
-           "    private val rangePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(77, 93, 121)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 4f" "    }"
-           "    private val rangeBandPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(80, 77, 93, 121)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 2f"
-           "        pathEffect = DashPathEffect(floatArrayOf(10f, 12f), 0f)"
-           "    }"
-           "    private val rangeBandTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(82, 94, 114)"
-           "        textSize = 16f" "        isFakeBoldText = true"
-           "        textAlign = Paint.Align.CENTER" "    }"
-           "    private val alphaFireLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(21, 96, 189)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 5f"
-           "        pathEffect = DashPathEffect(floatArrayOf(16f, 12f), 0f)"
-           "    }"
-           "    private val betaFireLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(184, 67, 54)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 5f"
-           "        pathEffect = DashPathEffect(floatArrayOf(16f, 12f), 0f)"
-           "    }"
-           "    private val fireSolutionPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(70, 167, 106)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 5f"
-           "        strokeCap = Paint.Cap.ROUND" "    }"
-           "    private val fireSolutionFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(44, 70, 167, 106)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val fireSolutionTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.WHITE" "        textSize = 18f"
-           "        isFakeBoldText = true"
-           "        textAlign = Paint.Align.CENTER" "    }"
-           "    private val weaponArcPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(28, 70, 167, 106)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val weaponArcStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(120, 70, 167, 106)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 2f" "    }"
-           "    private val overlayPanelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(218, 14, 22, 34)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val overlayBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(190, 92, 116, 150)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 2f" "    }"
-           "    private val overlayChipPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(42, 57, 78)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val overlayReadyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(65, 170, 104)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val overlayWaitPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(215, 145, 52)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val overlayTitlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.WHITE" "        textSize = 18f"
-           "        isFakeBoldText = true" "    }"
-           "    private val overlayTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(210, 222, 239)"
-           "        textSize = 15f" "        isFakeBoldText = true"
-           "    }"
-           "    private val overlayDimPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(157, 174, 198)"
-           "        textSize = 14f" "    }"
-           "    private val alphaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(21, 96, 189)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val betaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(184, 67, 54)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val seekerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(215, 131, 39)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val seekerTargetPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(215, 131, 39)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 4f"
-           "        pathEffect = DashPathEffect(floatArrayOf(10f, 12f), 0f)"
-           "    }"
-           "    private val shuttlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(106, 83, 168)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val webPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(63, 164, 210)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 5f" "    }"
-           "    private val webFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(58, 63, 164, 210)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val minePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(200, 57, 68)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val mineStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.WHITE"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 3f" "    }"
-           "    private val shipStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.WHITE"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 4f" "    }"
-           "    private val shipShadowStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(150, 20, 26, 36)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 7f"
-           "        strokeCap = Paint.Cap.ROUND"
-           "        strokeJoin = Paint.Join.ROUND" "    }"
-           "    private val shipDetailPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(185, 255, 255, 255)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 2.5f"
-           "        strokeCap = Paint.Cap.ROUND"
-           "        strokeJoin = Paint.Join.ROUND" "    }"
-           "    private val shipBadgePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.WHITE"
-           "        style = Paint.Style.FILL" "        textSize = 13f"
-           "        isFakeBoldText = true"
-           "        textAlign = Paint.Align.CENTER" "    }"
-           "    private val shipBadgeBackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(190, 20, 26, 36)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(28, 35, 48)"
-           "        textSize = 31f" "        isFakeBoldText = true"
-           "    }"
-           "    private val smallTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(49, 58, 73)"
-           "        textSize = 24f" "    }"
-           "    private val pillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(220, 255, 255, 255)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val shieldBackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(151, 160, 174)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 7f"
-           "        strokeCap = Paint.Cap.ROUND" "    }"
-           "    private val shieldPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(45, 137, 86)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 7f"
-           "        strokeCap = Paint.Cap.ROUND" "    }"
-           "    private val hitShieldPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(245, 185, 60)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 12f"
-           "        strokeCap = Paint.Cap.ROUND" "    }"
-           "    private val shieldNumberPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(28, 35, 48)"
-           "        textSize = 20f" "        isFakeBoldText = true"
-           "        textAlign = Paint.Align.CENTER" "    }"
-           "    private val paPanelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(214, 146, 45)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 8f"
-           "        strokeCap = Paint.Cap.ROUND" "    }"
-           "    private val paPanelFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(34, 214, 146, 45)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val movementTrailPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(145, 68, 82, 105)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 5f"
-           "        strokeCap = Paint.Cap.ROUND"
-           "        pathEffect = DashPathEffect(floatArrayOf(12f, 10f), 0f)"
-           "    }"
-           "    private val movementWakePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(100, 68, 82, 105)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 4f"
-           "        strokeCap = Paint.Cap.ROUND" "    }"
-           "    private val projectedPathPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(150, 21, 96, 189)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 4f"
-           "        strokeCap = Paint.Cap.ROUND"
-           "        strokeJoin = Paint.Join.ROUND"
-           "        pathEffect = DashPathEffect(floatArrayOf(8f, 8f), 0f)"
-           "    }"
-           "    private val projectedPointPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(210, 21, 96, 189)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val projectedTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(28, 35, 48)"
-           "        textSize = 17f" "        isFakeBoldText = true"
-           "        textAlign = Paint.Align.CENTER" "    }"
-           "    private val projectedRangePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(130, 68, 82, 105)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 3f"
-           "        strokeCap = Paint.Cap.ROUND"
-           "        pathEffect = DashPathEffect(floatArrayOf(7f, 9f), 0f)"
-           "    }"
-           "    private val projectedDangerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(207, 71, 62)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 5f"
-           "        strokeCap = Paint.Cap.ROUND" "    }"
-           "    private val projectedRangeTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(28, 35, 48)"
-           "        textSize = 16f" "        isFakeBoldText = true"
-           "        textAlign = Paint.Align.CENTER" "    }"
-           "    private val impactRingPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(245, 185, 60)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 8f" "    }"
-           "    private val impactFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(150, 245, 185, 60)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val blockedBadgePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.argb(232, 43, 31, 24)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val blockedBadgeBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(225, 171, 58)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 2.5f" "    }"
-           "    private val blockedBadgeTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.WHITE" "        textSize = 17f"
-           "        isFakeBoldText = true"
-           "        textAlign = Paint.Align.CENTER" "    }" ""
-           "    fun setGame(nextGame: JSONObject?, nextEvents: JSONArray = JSONArray()) {"
-           "        val previousBattle = game?.optJSONObject(\"battle-state\")"
-           "        val previousAlpha = previousBattle?.optJSONObject(\"alpha-position\")?.let { poseFrom(it) }"
-           "        val previousBeta = previousBattle?.optJSONObject(\"beta-position\")?.let { poseFrom(it) }"
-           "        val nextBattle = nextGame?.optJSONObject(\"battle-state\")"
-           "        val nextAlpha = nextBattle?.optJSONObject(\"alpha-position\")?.let { poseFrom(it) }"
-           "        val nextBeta = nextBattle?.optJSONObject(\"beta-position\")?.let { poseFrom(it) }"
-           "" "        game = nextGame" "        events = nextEvents"
-           "        if (nextGame == null) {"
-           "            events = JSONArray()"
-           "            tacticalStatus = null"
-           "            tacticalDashboard = null"
-           "            movementAnimator?.cancel()"
-           "            movementProgress = 1f"
-           "            clearImpactPulse()" "            resetView()"
-           "            return" "        }" ""
-           "        if (previousAlpha != null && previousBeta != null && nextAlpha != null && nextBeta != null &&"
-           "            (poseChanged(previousAlpha, nextAlpha) || poseChanged(previousBeta, nextBeta))"
-           "        ) {"
-           "            startMovementAnimation(previousAlpha, nextAlpha, previousBeta, nextBeta)"
-           "        } else {" "            finishMovementAnimation()"
-           "        }" "        updateImpactPulse(nextEvents)"
-           "        invalidate()" "    }" ""
-           "    fun setTacticalStatus(nextStatus: JSONObject?) {"
-           "        tacticalStatus = nextStatus" "        invalidate()"
-           "    }" ""
-           "    fun setTacticalDashboard(nextDashboard: JSONObject?) {"
-           "        tacticalDashboard = nextDashboard"
-           "        nextDashboard?.optJSONObject(\"status\")?.let { tacticalStatus = it }"
-           "        invalidate()" "    }" "" "    fun resetView() {"
-           "        userScale = 1f" "        userPanX = 0f"
-           "        userPanY = 0f" "        invalidate()" "    }" ""
-           "    override fun onTouchEvent(event: MotionEvent): Boolean {"
-           "        parent?.requestDisallowInterceptTouchEvent(true)"
-           "        scaleDetector.onTouchEvent(event)"
-           "        when (event.actionMasked) {"
-           "            MotionEvent.ACTION_DOWN -> {"
-           "                lastTouchX = event.x"
-           "                lastTouchY = event.y"
-           "                return true" "            }"
-           "            MotionEvent.ACTION_MOVE -> {"
-           "                if (!scaleDetector.isInProgress && event.pointerCount == 1) {"
-           "                    val scale = resources.displayMetrics.density.coerceAtLeast(1f)"
-           "                    userPanX += (event.x - lastTouchX) / scale"
-           "                    userPanY += (event.y - lastTouchY) / scale"
-           "                    lastTouchX = event.x"
-           "                    lastTouchY = event.y"
-           "                    invalidate()" "                }"
-           "                return true" "            }"
-           "            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {"
-           "                parent?.requestDisallowInterceptTouchEvent(false)"
-           "                return true" "            }" "        }"
-           "        return true" "    }" ""
-           "    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 battle = game?.optJSONObject(\"battle-state\")"
-           "        val alphaPosition = battle?.optJSONObject(\"alpha-position\")"
-           "        val betaPosition = battle?.optJSONObject(\"beta-position\")"
-           "        if (battle == null || alphaPosition == null || betaPosition == null) {"
-           "            drawEmptyState(canvas)" "            return"
-           "        }" ""
-           "        val alphaPose = animatedPose(poseFrom(alphaPosition), alphaMoveStart, alphaMoveEnd)"
-           "        val betaPose = animatedPose(poseFrom(betaPosition), betaMoveStart, betaMoveEnd)"
-           "        val orders = game?.optJSONObject(\"orders\")"
-           "        val currentImpulse = game?.optInt(\"impulse\", 0) ?: 0"
-           "        val alphaProjected = projectedMoves("
-           "            alphaPosition,"
-           "            maneuverMap(orders?.opt(\"alpha-maneuvers\")),"
-           "            currentImpulse,"
-           "            PROJECTED_MOVE_LIMIT" "        )"
-           "        val betaProjected = projectedMoves("
-           "            betaPosition,"
-           "            maneuverMap(orders?.opt(\"beta-maneuvers\")),"
-           "            currentImpulse,"
-           "            PROJECTED_MOVE_LIMIT" "        )"
-           "        val transform = mapTransform(alphaPose, betaPose, alphaProjected, betaProjected)"
-           "        drawGrid(canvas, transform, alphaPosition, betaPosition, alphaProjected, betaProjected)"
-           "        drawTerrain(canvas, transform, battle)" ""
-           "        val alphaPoint = transform.toPoint(alphaPose.q, alphaPose.r)"
-           "        val betaPoint = transform.toPoint(betaPose.q, betaPose.r)"
-           "        drawRangeBands(canvas, alphaPoint, transform, Color.rgb(21, 96, 189), -1)"
-           "        drawRangeBands(canvas, betaPoint, transform, Color.rgb(184, 67, 54), 1)"
-           "        drawRecentMovementWakes(canvas, transform)"
-           "        canvas.drawLine(alphaPoint.x, alphaPoint.y, betaPoint.x, betaPoint.y, rangePaint)"
-           "        drawWeaponArcFans(canvas, transform, alphaPose, tacticalStatus?.optJSONObject(\"alpha\"), Color.rgb(21, 96, 189))"
-           "        drawWeaponArcFans(canvas, transform, betaPose, tacticalStatus?.optJSONObject(\"beta\"), Color.rgb(184, 67, 54))"
-           "        drawFireSolutions(canvas, transform, alphaPoint, betaPoint)"
-           "        drawMovementTrails(canvas, transform)"
-           "        drawProjectedHelmPlots(canvas, transform, alphaPosition, betaPosition, alphaProjected, betaProjected)"
-           "        drawProjectedRangeForecast(canvas, transform, alphaPosition, betaPosition, alphaProjected, betaProjected)"
-           "        drawFireTargetLine(canvas, transform, alphaPosition, battle.optJSONObject(\"alpha-fire-target\"), alphaFireLinePaint)"
-           "        drawFireTargetLine(canvas, transform, betaPosition, battle.optJSONObject(\"beta-fire-target\"), betaFireLinePaint)"
-           "        drawSeekerTargetLines(canvas, transform, battle)"
-           ""
-           "        drawUnitCollection(canvas, transform, battle.opt(\"seeking-weapons\"), seekerPaint, \"S\")"
-           "        drawUnitCollection(canvas, transform, battle.opt(\"shuttles\"), shuttlePaint, \"U\")"
-           "" "        drawShip(" "            canvas,"
-           "            transform," "            \"ALPHA\","
-           "            alphaPose," "            alphaPosition,"
-           "            battle.optJSONObject(\"alpha\"),"
-           "            alphaPaint," "            -1,"
-           "            shieldFacingIndex(battle.opt(\"alpha-shield-facing-hit\"))"
-           "        )" "        drawShip(" "            canvas,"
-           "            transform," "            \"BETA\","
-           "            betaPose," "            betaPosition,"
-           "            battle.optJSONObject(\"beta\"),"
-           "            betaPaint," "            1,"
-           "            shieldFacingIndex(battle.opt(\"beta-shield-facing-hit\"))"
-           "        )"
-           "        drawImpactEffects(canvas, alphaPoint, betaPoint)"
-           "        drawBlockedManeuverBadges(canvas, alphaPoint, betaPoint)"
-           "        drawVolleyBadge(canvas, battle, betaPoint, \"alpha-volley\", \"A\", alphaPaint, -1)"
-           "        drawVolleyBadge(canvas, battle, alphaPoint, \"beta-volley\", \"B\", betaPaint, 1)"
-           "        drawBattleBadge(canvas, battle)"
-           "        drawTerrainBadge(canvas, battle)"
-           "        drawTacticalOverlay(canvas, battle)" "    }" ""
-           "    private fun drawTacticalOverlay(canvas: Canvas, battle: JSONObject) {"
-           "        val dashboard = tacticalDashboard"
-           "        val status = tacticalStatus ?: dashboard?.optJSONObject(\"status\")"
-           "        val activity = dashboard?.optJSONObject(\"impulse-activity\")"
-           "        val top = 8f" "        val left = 8f"
-           "        val width = (drawWidth * 0.58f).coerceIn(210f, drawWidth - 16f)"
-           "        val height = 84f"
-           "        val rect = RectF(left, top, left + width, top + height)"
-           "        canvas.drawRoundRect(rect, 9f, 9f, overlayPanelPaint)"
-           "        canvas.drawRoundRect(rect, 9f, 9f, overlayBorderPaint)"
-           "" "        overlayTitlePaint.textAlign = Paint.Align.LEFT"
-           "        val phaseImpulse = activity?.optInt(\"impulse\", status?.optInt(\"impulse\", 0) ?: 0) ?: (status?.optInt(\"impulse\", 0) ?: 0)"
-           "        val phaseTitle = \"IMPULSE $phaseImpulse\""
-           "        canvas.drawText(phaseTitle, rect.left + 9f, rect.top + 22f, overlayTitlePaint)"
-           "" "        overlayDimPaint.textAlign = Paint.Align.RIGHT"
-           "        val counts = status?.optJSONObject(\"counts\")"
-           "        val countText = \"S${counts?.optInt(\"active-seeking\", countUnits(battle.opt(\"seeking-weapons\"))) ?: countUnits(battle.opt(\"seeking-weapons\"))}\" +"
-           "            \" U${counts?.optInt(\"active-small-units\", countUnits(battle.opt(\"shuttles\"))) ?: countUnits(battle.opt(\"shuttles\"))}\" +"
-           "            \" W${counts?.optInt(\"active-webs\", 0) ?: 0}\" +"
-           "            \" M${counts?.optInt(\"active-mines\", 0) ?: 0}\""
-           "        canvas.drawText(countText, rect.right - 9f, rect.top + 21f, overlayDimPaint)"
-           ""
-           "        val phaseText = phaseSlotSummary(activity?.optJSONArray(\"stage-slots\"))"
-           "        overlayDimPaint.textAlign = Paint.Align.LEFT"
-           "        canvas.drawText(fitText(phaseText.ifBlank { \"No phase slot data yet\" }, overlayDimPaint, rect.width() - 18f), rect.left + 9f, rect.top + 43f, overlayDimPaint)"
-           ""
-           "        val alphaReady = availableActionCount(dashboard, \"alpha\")"
-           "        val alphaTotal = totalActionCount(dashboard, \"alpha\")"
-           "        val betaReady = availableActionCount(dashboard, \"beta\")"
-           "        val betaTotal = totalActionCount(dashboard, \"beta\")"
-           "        val chipTop = rect.bottom - 28f"
-           "        val chipMax = ((rect.width() - 30f) * 0.34f).coerceIn(82f, 150f)"
-           "        val alphaWidth = drawOverlayChip("
-           "            canvas," "            rect.left + 9f,"
-           "            chipTop,"
-           "            actionChipText(dashboard, \"alpha\", alphaReady, alphaTotal),"
-           "            alphaReady > 0," "            chipMax"
-           "        )"
-           "        val betaX = rect.left + 17f + alphaWidth"
-           "        val betaWidth = drawOverlayChip("
-           "            canvas," "            betaX,"
-           "            chipTop,"
-           "            actionChipText(dashboard, \"beta\", betaReady, betaTotal),"
-           "            betaReady > 0," "            chipMax"
-           "        )"
-           "        val nextEvent = latestCombatEventText()"
-           "        overlayDimPaint.textAlign = Paint.Align.RIGHT"
-           "        val eventLeft = betaX + betaWidth + 8f"
-           "        canvas.drawText(fitText(nextEvent, overlayDimPaint, rect.right - eventLeft - 9f), rect.right - 9f, chipTop + 17f, overlayDimPaint)"
-           "    }" ""
-           "    private fun drawOverlayChip(canvas: Canvas, x: Float, y: Float, text: String, ready: Boolean, maxWidth: Float): Float {"
-           "        val fitted = fitText(text, overlayTextPaint, maxWidth - 15f)"
-           "        val width = (overlayTextPaint.measureText(fitted) + 15f).coerceAtMost(maxWidth)"
-           "        val rect = RectF(x, y, x + width, y + 20f)"
-           "        canvas.drawRoundRect(rect, 10f, 10f, if (ready) overlayReadyPaint else overlayWaitPaint)"
-           "        overlayTextPaint.textAlign = Paint.Align.CENTER"
-           "        canvas.drawText(fitted, rect.centerX(), rect.top + 14.5f, overlayTextPaint)"
-           "        return width" "    }" ""
-           "    private fun phaseSlotSummary(slots: JSONArray?): String {"
-           "        if (slots == null || slots.length() == 0) return \"\""
-           "        val names = mutableListOf<String>()"
-           "        for (index in 0 until slots.length()) {"
-           "            val slot = slots.optJSONObject(index) ?: continue"
-           "            if (!slot.optBoolean(\"implemented?\", false)) continue"
-           "            val action = slot.optString(\"action\", \"\").ifBlank { slot.optString(\"engine-hook\", \"\") }"
-           "            if (action.isBlank()) continue"
-           "            val compact = action"
-           "                .replace(\"Launch \", \"Ln \")"
-           "                .replace(\"Fire Weapons\", \"Fire\")"
-           "                .replace(\"Announce \", \"\")"
-           "            if (!names.contains(compact)) names.add(compact)"
-           "            if (names.size >= 4) break" "        }"
-           "        return if (names.isEmpty()) \"Phase slots pending implementation\" else names.joinToString(\"  |  \")"
-           "    }" ""
-           "    private fun availableActionCount(dashboard: JSONObject?, actor: String): Int {"
-           "        val actions = dashboard?.optJSONObject(\"actions\")?.optJSONArray(actor) ?: return 0"
-           "        var count = 0"
-           "        for (index in 0 until actions.length()) {"
-           "            if (actions.optJSONObject(index)?.optBoolean(\"available?\", false) == true) count += 1"
-           "        }" "        return count" "    }" ""
-           "    private fun totalActionCount(dashboard: JSONObject?, actor: String): Int ="
-           "        dashboard?.optJSONObject(\"actions\")?.optJSONArray(actor)?.length() ?: 0"
-           ""
-           "    private fun actionChipText(dashboard: JSONObject?, actor: String, ready: Int, total: Int): String {"
-           "        val prefix = if (actor == \"alpha\") \"A\" else \"B\""
-           "        val labels = readyActionLabels(dashboard, actor)"
-           "        return if (labels.isEmpty()) \"$prefix CMD $ready/$total\" else \"$prefix ${labels.joinToString(\"/\")}\""
-           "    }" ""
-           "    private fun readyActionLabels(dashboard: JSONObject?, actor: String): List<String> {"
-           "        val actions = dashboard?.optJSONObject(\"actions\")?.optJSONArray(actor) ?: return emptyList()"
-           "        val readyById = linkedMapOf<String, JSONObject>()"
-           "        for (index in 0 until actions.length()) {"
-           "            val action = actions.optJSONObject(index) ?: continue"
-           "            val id = action.optString(\"id\", \"\")"
-           "            if (id.isNotBlank() && action.optBoolean(\"available?\", false)) readyById[id] = action"
-           "        }" "        return OVERLAY_ACTION_PRIORITY"
-           "            .filter { readyById.containsKey(it) }"
-           "            .map { overlayActionLabel(it) }"
-           "            .take(3)" "    }" ""
-           "    private fun overlayActionLabel(id: String): String ="
-           "        when (id) {"
-           "            \"direct-fire\" -> \"fire\""
-           "            \"seeking-launch\" -> \"seek\""
-           "            \"wild-weasel-preparation\" -> \"ww\""
-           "            \"suicide-shuttle\" -> \"suicide\""
-           "            \"scatter-pack\" -> \"scatter\""
-           "            \"shuttle-operations\" -> \"units\""
-           "            \"carrier-fighter-operations\" -> \"ftr\""
-           "            \"pf-operations\" -> \"pf\""
-           "            \"tractors\" -> \"tractor\""
-           "            \"transporters\" -> \"tran\""
-           "            \"electronic-warfare\" -> \"ew\""
-           "            \"shield-control\" -> \"shield\""
-           "            \"continuous-damage-repair\", \"emergency-damage-repair\", \"repair-system\" -> \"repair\""
-           "            \"web-actions\" -> \"web\""
-           "            \"mine-actions\" -> \"mine\""
-           "            else -> id.replace('-', ' ')" "        }" ""
-           "    private fun countUnits(raw: Any?): Int ="
-           "        when (raw) {"
-           "            is JSONArray -> raw.length()"
-           "            is JSONObject -> {"
-           "                if (raw.has(\"position\")) 1 else {"
-           "                    var total = 0"
-           "                    val keys = raw.keys()"
-           "                    while (keys.hasNext()) {"
-           "                        if (raw.opt(keys.next()) != null) total += 1"
-           "                    }" "                    total"
-           "                }" "            }" "            else -> 0"
-           "        }" ""
-           "    private fun shieldFacingIndex(value: Any?): Int ="
-           "        when (value) {"
-           "            is Number -> value.toInt()"
-           "            is String -> value.removePrefix(\"shield-\").toIntOrNull() ?: -1"
-           "            else -> -1"
-           "        }.takeIf { it in 1..6 } ?: -1" ""
-           "    private fun latestCombatEventText(): 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"
-           "            return type.replace('-', ' ').uppercase()"
-           "        }" "        return \"NO COMBAT EVENT\"" "    }" ""
-           "    private fun fitText(text: String, paint: Paint, maxWidth: Float): String {"
-           "        if (maxWidth <= 8f) return \"\""
-           "        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 fun drawRangeBands(canvas: Canvas, center: Point, transform: MapTransform, color: Int, labelSide: Int) {"
-           "        rangeBandPaint.color = Color.argb(76, Color.red(color), Color.green(color), Color.blue(color))"
-           "        rangeBandTextPaint.color = Color.argb(190, Color.red(color), Color.green(color), Color.blue(color))"
-           "        RANGE_BANDS.forEach { band ->"
-           "            val radius = hexCenterDistance(transform, band)"
-           "            if (radius < 18f) return@forEach"
-           "            canvas.drawCircle(center.x, center.y, radius, rangeBandPaint)"
-           "            val labelX = (center.x + labelSide * radius * 0.70f).coerceIn(18f, drawWidth - 18f)"
-           "            val labelY = (center.y - radius * 0.70f).coerceIn(20f, drawHeight - 8f)"
-           "            canvas.drawText(\"R$band\", labelX, labelY, rangeBandTextPaint)"
-           "        }"
-           "        rangeBandTextPaint.color = Color.rgb(82, 94, 114)"
-           "    }" ""
-           "    private fun hexCenterDistance(transform: MapTransform, range: Int): Float ="
-           "        (sqrt(3.0) * range.toDouble()).toFloat() * transform.scale"
-           ""
-           "    private fun drawFireSolutions(canvas: Canvas, transform: MapTransform, alphaPoint: Point, betaPoint: Point) {"
-           "        val status = tacticalStatus ?: return"
-           "        drawFireSolution(canvas, alphaPoint, betaPoint, actorFireSolution(status.optJSONObject(\"alpha\")), Color.rgb(21, 96, 189), -1)"
-           "        drawFireSolution(canvas, betaPoint, alphaPoint, actorFireSolution(status.optJSONObject(\"beta\")), Color.rgb(184, 67, 54), 1)"
-           "    }" "" "    private fun drawFireSolution("
-           "        canvas: Canvas," "        source: Point,"
-           "        target: Point," "        solution: FireSolution,"
-           "        sideColor: Int," "        labelSide: Int" "    ) {"
-           "        if (solution.installed <= 0) return"
-           "        val color = when {"
-           "            solution.inArc > 0 -> Color.rgb(70, 167, 106)"
-           "            solution.ready > 0 || solution.armed > 0 -> Color.rgb(230, 155, 63)"
-           "            else -> Color.rgb(126, 137, 155)" "        }"
-           "        fireSolutionPaint.color = Color.argb(160, Color.red(color), Color.green(color), Color.blue(color))"
-           "        fireSolutionPaint.pathEffect = when {"
-           "            solution.inArc > 0 -> null"
-           "            solution.ready > 0 || solution.armed > 0 -> DashPathEffect(floatArrayOf(18f, 10f), 0f)"
-           "            else -> DashPathEffect(floatArrayOf(6f, 12f), 0f)"
-           "        }"
-           "        fireSolutionFillPaint.color = Color.argb(34, Color.red(sideColor), Color.green(sideColor), Color.blue(sideColor))"
-           ""
-           "        canvas.drawLine(source.x, source.y, target.x, target.y, fireSolutionPaint)"
-           "        canvas.drawCircle(target.x, target.y, 18f, fireSolutionPaint)"
-           "        canvas.drawCircle(source.x, source.y, 24f, fireSolutionFillPaint)"
-           "" "        val midpointX = (source.x + target.x) / 2f"
-           "        val midpointY = (source.y + target.y) / 2f"
-           "        val text = when {"
-           "            solution.inArc > 0 -> \"LOCK ${solution.inArc}/${solution.ready}\""
-           "            solution.ready > 0 -> \"OUT ARC ${solution.ready}\""
-           "            solution.armed > 0 -> \"ARMED ${solution.armed}\""
-           "            else -> \"COLD\"" "        }"
-           "        val width = fireSolutionTextPaint.measureText(text) + 18f"
-           "        val x = (midpointX + labelSide * 34f).coerceIn(width / 2f + 6f, drawWidth - width / 2f - 6f)"
-           "        val y = (midpointY - 18f).coerceIn(24f, drawHeight - 8f)"
-           "        canvas.drawRoundRect(RectF(x - width / 2f, y - 20f, x + width / 2f, y + 5f), 9f, 9f, pillPaint)"
-           "        fireSolutionTextPaint.color = color"
-           "        canvas.drawText(text, x, y - 3f, fireSolutionTextPaint)"
-           "        fireSolutionTextPaint.color = Color.WHITE"
-           "        fireSolutionPaint.pathEffect = null" "    }" ""
-           "    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 drawWeaponArcFans("
-           "        canvas: Canvas," "        transform: MapTransform,"
-           "        pose: ShipPose," "        actor: JSONObject?,"
-           "        sideColor: Int" "    ) {"
-           "        val fans = actorWeaponArcFans(actor)"
-           "        if (fans.isEmpty()) return"
-           "        val center = transform.toPoint(pose.q, pose.r)"
-           "        val radius = hexCenterDistance(transform, 5).coerceAtLeast(transform.scale * 2.4f)"
-           "        val facingDegrees = (facingAngleRadians(pose.facing) * 180.0 / PI).toFloat()"
-           "        fans.forEach { fan ->"
-           "            val alpha = when (fan.strength) {"
-           "                3 -> 42" "                2 -> 28"
-           "                else -> 18" "            }"
-           "            weaponArcPaint.color = Color.argb(alpha, Color.red(sideColor), Color.green(sideColor), Color.blue(sideColor))"
-           "            weaponArcStrokePaint.color = Color.argb(alpha * 3, Color.red(sideColor), Color.green(sideColor), Color.blue(sideColor))"
-           "            drawWeaponArcFan(canvas, center, radius, facingDegrees, fan.code)"
-           "        }" "    }" ""
-           "    private fun drawWeaponArcFan(canvas: Canvas, center: Point, radius: Float, facingDegrees: Float, code: String) {"
-           "        val sectors = firingArcSectors(code)"
-           "        if (sectors.isEmpty()) return"
-           "        val bounds = RectF(center.x - radius, center.y - radius, center.x + radius, center.y + radius)"
-           "        sectors.forEach { sector ->"
-           "            if (sector.sweep >= 359f) {"
-           "                canvas.drawCircle(center.x, center.y, radius, weaponArcPaint)"
-           "                canvas.drawCircle(center.x, center.y, radius, weaponArcStrokePaint)"
-           "            } else {"
-           "                val start = facingDegrees + sector.centerOffset - sector.sweep / 2f"
-           "                canvas.drawArc(bounds, start, sector.sweep, true, weaponArcPaint)"
-           "                canvas.drawArc(bounds, start, sector.sweep, true, weaponArcStrokePaint)"
-           "            }" "        }" "    }" ""
-           "    private fun actorWeaponArcFans(actor: JSONObject?): List<ArcFan> {"
-           "        val weapons = actor?.optJSONArray(\"weapon-status\") ?: return emptyList()"
-           "        val strengths = linkedMapOf<String, Int>()"
-           "        for (index in 0 until weapons.length()) {"
-           "            val weapon = weapons.optJSONObject(index) ?: continue"
-           "            if (weapon.optInt(\"installed\", 0) <= 0) continue"
-           "            val strength = when {"
-           "                weapon.optInt(\"fireable-in-arc\", 0) > 0 -> 3"
-           "                weapon.optInt(\"fireable\", 0) > 0 -> 2"
-           "                weapon.optInt(\"armed\", 0) > 0 -> 1"
-           "                else -> 0" "            }"
-           "            if (strength <= 0) continue"
-           "            val arcs = weapon.optJSONArray(\"arc-codes\") ?: JSONArray().put(weapon.optString(\"arc\", \"\"))"
-           "            for (arcIndex in 0 until arcs.length()) {"
-           "                val code = arcs.optString(arcIndex, \"\").trim().uppercase()"
-           "                if (code.isBlank()) continue"
-           "                strengths[code] = maxOf(strengths[code] ?: 0, strength)"
-           "            }" "        }"
-           "        return strengths.entries"
-           "            .sortedWith(compareByDescending<Map.Entry<String, Int>> { it.value }.thenBy { arcSortBucket(it.key) }.thenBy { it.key })"
-           "            .take(4)"
-           "            .map { ArcFan(it.key, it.value) }" "    }" ""
-           "    private fun firingArcSectors(code: String): List<ArcSector> {"
-           "        val normalized = code.uppercase().replace(\" \", \"\")"
-           "        return when {"
-           "            normalized == \"360\" || normalized == \"ALL\" -> listOf(ArcSector(0f, 360f))"
-           "            normalized == \"FA\" || normalized == \"F\" -> listOf(ArcSector(0f, 120f))"
-           "            normalized == \"FH\" -> listOf(ArcSector(0f, 180f))"
-           "            normalized == \"RA\" || normalized == \"A\" -> listOf(ArcSector(180f, 120f))"
-           "            normalized == \"RH\" -> listOf(ArcSector(180f, 180f))"
-           "            normalized == \"LF\" -> listOf(ArcSector(-60f, 120f))"
-           "            normalized == \"RF\" -> listOf(ArcSector(60f, 120f))"
-           "            normalized == \"L\" || normalized == \"LS\" -> listOf(ArcSector(-90f, 180f))"
-           "            normalized == \"R\" || normalized == \"RS\" -> listOf(ArcSector(90f, 180f))"
-           "            normalized == \"LP\" || normalized == \"PORT\" -> listOf(ArcSector(-90f, 120f))"
-           "            normalized == \"RP\" || normalized == \"STARBOARD\" -> listOf(ArcSector(90f, 120f))"
-           "            \"+\" in normalized -> normalized.split(\"+\").flatMap { firingArcSectors(it) }"
-           "            \"/\" in normalized -> normalized.split(\"/\").flatMap { firingArcSectors(it) }"
-           "            else -> emptyList()" "        }" "    }" ""
-           "    private fun arcSortBucket(code: String): Int ="
-           "        when (code.uppercase()) {"
-           "            \"FA\", \"F\", \"FH\" -> 0"
-           "            \"LF\", \"RF\", \"L\", \"R\", \"LS\", \"RS\" -> 1"
-           "            \"RA\", \"RH\", \"A\" -> 2"
-           "            \"360\", \"ALL\" -> 3" "            else -> 4"
-           "        }" ""
-           "    private fun drawEmptyState(canvas: Canvas) {"
-           "        labelPaint.textAlign = Paint.Align.CENTER"
-           "        canvas.drawText(\"No active combat\", drawWidth / 2f, drawHeight / 2f - 12f, labelPaint)"
-           "        smallTextPaint.textAlign = Paint.Align.CENTER"
-           "        canvas.drawText(\"Create or restore a game\", drawWidth / 2f, drawHeight / 2f + 24f, smallTextPaint)"
-           "    }" "" "    private fun mapTransform("
-           "        alpha: ShipPose," "        beta: ShipPose,"
-           "        alphaProjected: List<ProjectedMove> = emptyList(),"
-           "        betaProjected: List<ProjectedMove> = emptyList()"
-           "    ): MapTransform {"
-           "        val points = mutableListOf(rawHexPoint(alpha.q, alpha.r), rawHexPoint(beta.q, beta.r))"
-           "        alphaProjected.forEach { points.add(rawHexPoint(it.pose.q, it.pose.r)) }"
-           "        betaProjected.forEach { points.add(rawHexPoint(it.pose.q, it.pose.r)) }"
-           "        val minX = points.minOf { it.x }"
-           "        val maxX = points.maxOf { it.x }"
-           "        val minY = points.minOf { it.y }"
-           "        val maxY = points.maxOf { it.y }"
-           "        val spanX = max(4f, maxX - minX + 6f)"
-           "        val spanY = max(4f, maxY - minY + 5f)"
-           "        val scale = min(drawWidth / spanX, drawHeight / spanY).coerceIn(16f, 64f) * userScale"
-           "        val centerRaw = Point((minX + maxX) / 2f, (minY + maxY) / 2f)"
-           "        val origin = Point("
-           "            drawWidth / 2f - centerRaw.x * scale + userPanX,"
-           "            drawHeight / 2f - centerRaw.y * scale + userPanY"
-           "        )" "        return MapTransform(scale, origin)"
-           "    }" "" "    private fun drawGrid("
-           "        canvas: Canvas," "        transform: MapTransform,"
-           "        alpha: JSONObject," "        beta: JSONObject,"
-           "        alphaProjected: List<ProjectedMove> = emptyList(),"
-           "        betaProjected: List<ProjectedMove> = emptyList()"
-           "    ) {"
-           "        var minQ = min(alpha.optInt(\"q\"), beta.optInt(\"q\"))"
-           "        var maxQ = max(alpha.optInt(\"q\"), beta.optInt(\"q\"))"
-           "        var minR = min(alpha.optInt(\"r\"), beta.optInt(\"r\"))"
-           "        var maxR = max(alpha.optInt(\"r\"), beta.optInt(\"r\"))"
-           "        (alphaProjected + betaProjected).forEach { step ->"
-           "            minQ = min(minQ, step.pose.q)"
-           "            maxQ = max(maxQ, step.pose.q)"
-           "            minR = min(minR, step.pose.r)"
-           "            maxR = max(maxR, step.pose.r)" "        }"
-           "        minQ -= 7" "        maxQ += 7" "        minR -= 7"
-           "        maxR += 7" "        for (q in minQ..maxQ) {"
-           "            for (r in minR..maxR) {"
-           "                val center = transform.toPoint(q, r)"
-           "                if (center.x < -transform.scale || center.x > drawWidth + transform.scale) continue"
-           "                if (center.y < -transform.scale || center.y > drawHeight + transform.scale) continue"
-           "                drawHex(canvas, center.x, center.y, transform.scale * 0.94f, gridPaint)"
-           "                if (q == 0 && r == 0) {"
-           "                    drawHex(canvas, center.x, center.y, transform.scale * 0.94f, strongGridPaint)"
-           "                }" "            }" "        }" "    }" ""
-           "    private fun drawHex(canvas: Canvas, cx: Float, cy: Float, radius: Float, paint: Paint) {"
-           "        val path = Path()" "        for (i in 0 until 6) {"
-           "            val angle = (-90.0 + i * 60.0) * PI / 180.0"
-           "            val x = cx + (cos(angle) * radius).toFloat()"
-           "            val y = cy + (sin(angle) * radius).toFloat()"
-           "            if (i == 0) path.moveTo(x, y) else path.lineTo(x, y)"
-           "        }" "        path.close()"
-           "        canvas.drawPath(path, paint)" "    }" ""
-           "    private fun drawShip(" "        canvas: Canvas,"
-           "        transform: MapTransform," "        label: String,"
-           "        pose: ShipPose," "        position: JSONObject,"
-           "        ship: JSONObject?," "        fillPaint: Paint,"
-           "        labelSide: Int," "        hitShield: Int" "    ) {"
-           "        val center = transform.toPoint(pose.q, pose.r)"
-           "        val radius = transform.scale * 0.54f"
-           "        val angle = facingAngleRadians(pose.facing)"
-           "        val dx = cos(angle).toFloat()"
-           "        val dy = sin(angle).toFloat()"
-           "        val px = -dy" "        val py = dx" ""
-           "        drawShieldRing(canvas, center, radius * 1.35f, ship, hitShield)"
-           "        drawFacingGuide(canvas, center, radius * 1.9f, angle, fillPaint)"
-           ""
-           "        drawRaceShipSilhouette(canvas, center, radius, angle, ship, fillPaint)"
-           "        drawRaceBadge(canvas, center, radius, ship)" ""
-           "        val labelX = (center.x + labelSide * transform.scale * 1.25f).coerceIn(8f, drawWidth - 8f)"
-           "        val labelY = (center.y - transform.scale * 1.15f).coerceIn(34f, drawHeight - 12f)"
-           "        drawShipLabel(canvas, label, labelX, labelY, ship, position, labelSide)"
-           "    }" "" "    private fun drawRaceShipSilhouette("
-           "        canvas: Canvas," "        center: Point,"
-           "        radius: Float," "        angle: Double,"
-           "        ship: JSONObject?," "        fillPaint: Paint"
-           "    ) {" "        val state = canvas.save()"
-           "        canvas.translate(center.x, center.y)"
-           "        canvas.rotate((angle * 180.0 / PI).toFloat())"
-           "        val iconRadius = radius * 1.24f"
-           "        when (shipRaceKey(ship)) {"
-           "            \"federation\" -> drawFederationSilhouette(canvas, iconRadius, fillPaint)"
-           "            \"klingon\" -> drawKlingonSilhouette(canvas, iconRadius, fillPaint)"
-           "            \"romulan\" -> drawRomulanSilhouette(canvas, iconRadius, fillPaint)"
-           "            \"kzinti\" -> drawKzintiSilhouette(canvas, iconRadius, fillPaint)"
-           "            \"andromedan\" -> drawAndromedanSilhouette(canvas, iconRadius, fillPaint)"
-           "            \"tholian\" -> drawTholianSilhouette(canvas, iconRadius, fillPaint)"
-           "            \"gorn\" -> drawGornSilhouette(canvas, iconRadius, fillPaint)"
-           "            \"hydran\" -> drawHydranSilhouette(canvas, iconRadius, fillPaint)"
-           "            \"lyran\" -> drawLyranSilhouette(canvas, iconRadius, fillPaint)"
-           "            \"isc\" -> drawIscSilhouette(canvas, iconRadius, fillPaint)"
-           "            \"orion\" -> drawOrionSilhouette(canvas, iconRadius, fillPaint)"
-           "            else -> drawGenericSilhouette(canvas, iconRadius, fillPaint)"
-           "        }" "        canvas.restoreToCount(state)" "    }"
-           ""
-           "    private fun drawFederationSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {"
-           "        canvas.drawRoundRect(RectF(-r * 0.88f, -r * 0.14f, r * 0.10f, r * 0.14f), r * 0.10f, r * 0.10f, fillPaint)"
-           "        canvas.drawRoundRect(RectF(-r * 0.92f, -r * 0.70f, r * 0.18f, -r * 0.48f), r * 0.12f, r * 0.12f, fillPaint)"
-           "        canvas.drawRoundRect(RectF(-r * 0.92f, r * 0.48f, r * 0.18f, r * 0.70f), r * 0.12f, r * 0.12f, fillPaint)"
-           "        canvas.drawOval(RectF(-r * 0.08f, -r * 0.58f, r * 1.10f, r * 0.58f), fillPaint)"
-           "        canvas.drawRoundRect(RectF(-r * 0.88f, -r * 0.14f, r * 0.10f, r * 0.14f), r * 0.10f, r * 0.10f, shipStrokePaint)"
-           "        canvas.drawRoundRect(RectF(-r * 0.92f, -r * 0.70f, r * 0.18f, -r * 0.48f), r * 0.12f, r * 0.12f, shipStrokePaint)"
-           "        canvas.drawRoundRect(RectF(-r * 0.92f, r * 0.48f, r * 0.18f, r * 0.70f), r * 0.12f, r * 0.12f, shipStrokePaint)"
-           "        canvas.drawOval(RectF(-r * 0.08f, -r * 0.58f, r * 1.10f, r * 0.58f), shipStrokePaint)"
-           "        canvas.drawLine(r * 0.16f, 0f, r * 0.84f, 0f, shipDetailPaint)"
-           "    }" ""
-           "    private fun drawKlingonSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {"
-           "        val wings = Path().apply {"
-           "            moveTo(r * 0.10f, -r * 0.14f)"
-           "            lineTo(-r * 1.06f, -r * 0.78f)"
-           "            lineTo(-r * 0.70f, -r * 0.18f)"
-           "            lineTo(-r * 0.20f, 0f)"
-           "            lineTo(-r * 0.70f, r * 0.18f)"
-           "            lineTo(-r * 1.06f, r * 0.78f)"
-           "            lineTo(r * 0.10f, r * 0.14f)"
-           "            close()" "        }"
-           "        canvas.drawPath(wings, fillPaint)"
-           "        canvas.drawPath(wings, shipStrokePaint)"
-           "        canvas.drawRoundRect(RectF(-r * 0.18f, -r * 0.10f, r * 0.76f, r * 0.10f), r * 0.08f, r * 0.08f, fillPaint)"
-           "        canvas.drawRoundRect(RectF(r * 0.58f, -r * 0.28f, r * 1.10f, r * 0.28f), r * 0.14f, r * 0.14f, fillPaint)"
-           "        canvas.drawRoundRect(RectF(-r * 0.18f, -r * 0.10f, r * 0.76f, r * 0.10f), r * 0.08f, r * 0.08f, shipStrokePaint)"
-           "        canvas.drawRoundRect(RectF(r * 0.58f, -r * 0.28f, r * 1.10f, r * 0.28f), r * 0.14f, r * 0.14f, shipStrokePaint)"
-           "    }" ""
-           "    private fun drawRomulanSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {"
-           "        val path = Path().apply {"
-           "            moveTo(r * 1.18f, 0f)"
-           "            lineTo(r * 0.18f, -r * 0.30f)"
-           "            lineTo(-r * 1.12f, -r * 0.88f)"
-           "            lineTo(-r * 0.64f, 0f)"
-           "            lineTo(-r * 1.12f, r * 0.88f)"
-           "            lineTo(r * 0.18f, r * 0.30f)"
-           "            close()" "        }"
-           "        canvas.drawPath(path, fillPaint)"
-           "        canvas.drawPath(path, shipStrokePaint)"
-           "        canvas.drawLine(-r * 0.64f, 0f, r * 0.72f, 0f, shipDetailPaint)"
-           "    }" ""
-           "    private fun drawKzintiSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {"
-           "        val port = Path().apply {"
-           "            moveTo(r * 0.95f, -r * 0.45f)"
-           "            lineTo(-r * 0.96f, -r * 0.70f)"
-           "            lineTo(-r * 0.60f, -r * 0.20f)"
-           "            lineTo(r * 0.76f, -r * 0.12f)"
-           "            close()" "        }"
-           "        val starboard = Path().apply {"
-           "            moveTo(r * 0.95f, r * 0.45f)"
-           "            lineTo(-r * 0.96f, r * 0.70f)"
-           "            lineTo(-r * 0.60f, r * 0.20f)"
-           "            lineTo(r * 0.76f, r * 0.12f)"
-           "            close()" "        }"
-           "        val center = Path().apply {"
-           "            moveTo(r * 1.05f, 0f)"
-           "            lineTo(-r * 0.38f, -r * 0.22f)"
-           "            lineTo(-r * 0.70f, 0f)"
-           "            lineTo(-r * 0.38f, r * 0.22f)"
-           "            close()" "        }"
-           "        drawFilledPath(canvas, port, fillPaint)"
-           "        drawFilledPath(canvas, starboard, fillPaint)"
-           "        drawFilledPath(canvas, center, fillPaint)" "    }"
-           ""
-           "    private fun drawAndromedanSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {"
-           "        canvas.drawOval(RectF(-r * 1.08f, -r * 0.50f, r * 1.08f, r * 0.50f), fillPaint)"
-           "        canvas.drawOval(RectF(-r * 1.08f, -r * 0.50f, r * 1.08f, r * 0.50f), shipStrokePaint)"
-           "        canvas.drawOval(RectF(-r * 0.34f, -r * 0.30f, r * 0.34f, r * 0.30f), shipDetailPaint)"
-           "        canvas.drawCircle(-r * 0.70f, 0f, r * 0.18f, shipDetailPaint)"
-           "        canvas.drawCircle(r * 0.70f, 0f, r * 0.18f, shipDetailPaint)"
-           "    }" ""
-           "    private fun drawTholianSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {"
-           "        val path = Path().apply {"
-           "            moveTo(r * 1.10f, 0f)"
-           "            lineTo(0f, -r * 0.92f)"
-           "            lineTo(-r * 1.02f, 0f)"
-           "            lineTo(0f, r * 0.92f)" "            close()"
-           "        }"
-           "        drawFilledPath(canvas, path, fillPaint)"
-           "        canvas.drawLine(-r * 0.62f, 0f, r * 0.62f, 0f, shipDetailPaint)"
-           "        canvas.drawLine(0f, -r * 0.56f, 0f, r * 0.56f, shipDetailPaint)"
-           "    }" ""
-           "    private fun drawGornSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {"
-           "        canvas.drawRoundRect(RectF(-r * 0.92f, -r * 0.46f, r * 0.28f, r * 0.46f), r * 0.13f, r * 0.13f, fillPaint)"
-           "        canvas.drawRoundRect(RectF(-r * 0.92f, -r * 0.46f, r * 0.28f, r * 0.46f), r * 0.13f, r * 0.13f, shipStrokePaint)"
-           "        val head = Path().apply {"
-           "            moveTo(r * 1.08f, 0f)"
-           "            lineTo(r * 0.20f, -r * 0.42f)"
-           "            lineTo(r * 0.20f, r * 0.42f)"
-           "            close()" "        }"
-           "        drawFilledPath(canvas, head, fillPaint)"
-           "        canvas.drawLine(-r * 0.72f, 0f, r * 0.18f, 0f, shipDetailPaint)"
-           "    }" ""
-           "    private fun drawHydranSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {"
-           "        canvas.drawRoundRect(RectF(-r * 0.88f, -r * 0.18f, r * 1.00f, r * 0.18f), r * 0.12f, r * 0.12f, fillPaint)"
-           "        canvas.drawRoundRect(RectF(-r * 0.88f, -r * 0.18f, r * 1.00f, r * 0.18f), r * 0.12f, r * 0.12f, shipStrokePaint)"
-           "        canvas.drawOval(RectF(-r * 0.75f, -r * 0.76f, r * 0.48f, -r * 0.34f), fillPaint)"
-           "        canvas.drawOval(RectF(-r * 0.75f, r * 0.34f, r * 0.48f, r * 0.76f), fillPaint)"
-           "        canvas.drawOval(RectF(-r * 0.75f, -r * 0.76f, r * 0.48f, -r * 0.34f), shipStrokePaint)"
-           "        canvas.drawOval(RectF(-r * 0.75f, r * 0.34f, r * 0.48f, r * 0.76f), shipStrokePaint)"
-           "    }" ""
-           "    private fun drawLyranSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {"
-           "        val port = Path().apply {"
-           "            moveTo(r * 0.95f, -r * 0.28f)"
-           "            lineTo(-r * 0.98f, -r * 0.68f)"
-           "            lineTo(-r * 0.74f, -r * 0.18f)"
-           "            lineTo(r * 0.82f, -r * 0.08f)"
-           "            close()" "        }"
-           "        val starboard = Path().apply {"
-           "            moveTo(r * 0.95f, r * 0.28f)"
-           "            lineTo(-r * 0.98f, r * 0.68f)"
-           "            lineTo(-r * 0.74f, r * 0.18f)"
-           "            lineTo(r * 0.82f, r * 0.08f)"
-           "            close()" "        }"
-           "        drawFilledPath(canvas, port, fillPaint)"
-           "        drawFilledPath(canvas, starboard, fillPaint)"
-           "        canvas.drawLine(-r * 0.52f, 0f, r * 0.78f, 0f, shipDetailPaint)"
-           "    }" ""
-           "    private fun drawIscSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {"
-           "        canvas.drawOval(RectF(-r * 0.82f, -r * 0.20f, r * 1.00f, r * 0.20f), fillPaint)"
-           "        canvas.drawOval(RectF(-r * 0.82f, -r * 0.20f, r * 1.00f, r * 0.20f), shipStrokePaint)"
-           "        canvas.drawOval(RectF(-r * 0.74f, -r * 0.76f, r * 0.38f, -r * 0.38f), fillPaint)"
-           "        canvas.drawOval(RectF(-r * 0.74f, r * 0.38f, r * 0.38f, r * 0.76f), fillPaint)"
-           "        canvas.drawOval(RectF(-r * 0.74f, -r * 0.76f, r * 0.38f, -r * 0.38f), shipStrokePaint)"
-           "        canvas.drawOval(RectF(-r * 0.74f, r * 0.38f, r * 0.38f, r * 0.76f), shipStrokePaint)"
-           "        canvas.drawCircle(r * 0.78f, 0f, r * 0.20f, shipDetailPaint)"
-           "    }" ""
-           "    private fun drawOrionSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {"
-           "        val path = Path().apply {"
-           "            moveTo(r * 1.12f, 0f)"
-           "            lineTo(-r * 0.95f, -r * 0.56f)"
-           "            lineTo(-r * 0.52f, 0f)"
-           "            lineTo(-r * 0.95f, r * 0.56f)"
-           "            close()" "        }"
-           "        drawFilledPath(canvas, path, fillPaint)"
-           "        canvas.drawLine(-r * 0.45f, 0f, r * 0.76f, 0f, shipDetailPaint)"
-           "    }" ""
-           "    private fun drawGenericSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {"
-           "        val path = Path().apply {"
-           "            moveTo(r * 1.35f, 0f)"
-           "            lineTo(-r * 0.85f, r * 0.90f)"
-           "            lineTo(-r * 0.45f, 0f)"
-           "            lineTo(-r * 0.85f, -r * 0.90f)"
-           "            close()" "        }"
-           "        drawFilledPath(canvas, path, fillPaint)" "    }" ""
-           "    private fun drawFilledPath(canvas: Canvas, path: Path, fillPaint: Paint) {"
-           "        canvas.drawPath(path, fillPaint)"
-           "        canvas.drawPath(path, shipShadowStrokePaint)"
-           "        canvas.drawPath(path, shipStrokePaint)" "    }" ""
-           "    private fun drawRaceBadge(canvas: Canvas, center: Point, radius: Float, ship: JSONObject?) {"
-           "        val badge = shipRaceBadge(ship)"
-           "        if (badge.isBlank()) return"
-           "        val width = shipBadgePaint.measureText(badge) + 12f"
-           "        val height = 17f"
-           "        val y = center.y + radius * 0.95f"
-           "        val rect = RectF(center.x - width / 2f, y - height / 2f, center.x + width / 2f, y + height / 2f)"
-           "        canvas.drawRoundRect(rect, 6f, 6f, shipBadgeBackPaint)"
-           "        canvas.drawText(badge, center.x, y + 4.5f, shipBadgePaint)"
-           "    }" ""
-           "    private fun shipRaceBadge(ship: JSONObject?): String ="
-           "        when (shipRaceKey(ship)) {"
-           "            \"andromedan\" -> \"AND\""
-           "            \"federation\" -> \"FED\""
-           "            \"klingon\" -> \"KLI\""
-           "            \"romulan\" -> \"ROM\""
-           "            \"kzinti\" -> \"KZN\""
-           "            \"tholian\" -> \"THO\""
-           "            \"gorn\" -> \"GOR\""
-           "            \"hydran\" -> \"HYD\""
-           "            \"lyran\" -> \"LYR\""
-           "            \"isc\" -> \"ISC\""
-           "            \"orion\" -> \"ORI\""
-           "            else -> \"\"" "        }" ""
-           "    private fun shipRaceKey(ship: JSONObject?): String {"
-           "        val properties = ship?.optJSONObject(\"properties\")"
-           "        val text = listOf("
-           "            properties?.optString(\"race\", \"\"),"
-           "            properties?.optString(\"empire\", \"\"),"
-           "            properties?.optString(\"ship-group\", \"\"),"
-           "            ship?.optString(\"template-id\", \"\"),"
-           "            ship?.optString(\"id\", \"\"),"
-           "            ship?.optString(\"name\", \"\")"
-           "        ).joinToString(\" \").lowercase()"
-           "        return when {"
-           "            \"andromedan\" in text || \"and-\" in text || \"andro\" in text -> \"andromedan\""
-           "            \"federation\" in text || \"fed-\" in text || \" starfleet \" in \" $text \" -> \"federation\""
-           "            \"klingon\" in text || \"klingon-\" in text || \"kli-\" in text -> \"klingon\""