Use typed SSD damage view

ober

cf4dc931bd48399856225a4c704362be0c045da4

diff --git a/templates/original-tactics.ss b/templates/original-tactics.ss
index 6606266..42f9104 100644
--- a/templates/original-tactics.ss
+++ b/templates/original-tactics.ss
@@ -3575,642 +3575,8 @@
                                        ship
                                        (int32 18)
                                        fallback)))))))))))
-        (kotlin-file-lines
-          "com/jerboa/originaltactics/SsdDamageView.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"
-           "import kotlin.math.roundToInt" ""
-           "class SsdDamageView(context: Context) : View(context) {"
-           "    private var game: JSONObject? = null"
-           "    private var alphaTruth: JSONObject? = null"
-           "    private var betaTruth: JSONObject? = null"
-           "    private var drawWidth = 0f"
-           "    private var drawHeight = 0f" ""
-           "    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(12, 17, 25)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val panelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(24, 32, 44)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(75, 90, 116)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 2f" "    }"
-           "    private val headerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(45, 61, 83)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val boxPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(85, 103, 130)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val lostPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(211, 81, 68)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val outlinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(169, 184, 207)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 1.6f" "    }"
-           "    private val recentHitPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(246, 190, 72)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 3.4f" "    }"
-           "    private val bandPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(18, 25, 36)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val shieldTrackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(43, 54, 70)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val shieldValuePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(91, 171, 209)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val arcInactivePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(38, 49, 65)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val arcActivePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(102, 190, 220)"
-           "        style = Paint.Style.FILL" "    }"
-           "    private val arcStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(154, 171, 193)"
-           "        style = Paint.Style.STROKE"
-           "        strokeWidth = 1f" "    }"
-           "    private val titlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.WHITE" "        textSize = 24f"
-           "        isFakeBoldText = true" "    }"
-           "    private val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(230, 237, 247)"
-           "        textSize = 17f" "        isFakeBoldText = true"
-           "    }"
-           "    private val smallPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-           "        color = Color.rgb(161, 177, 199)"
-           "        textSize = 15f" "    }" ""
-           "    fun setData(nextGame: JSONObject?, alpha: JSONObject?, beta: JSONObject?) {"
-           "        game = nextGame" "        alphaTruth = alpha"
-           "        betaTruth = beta" "        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 battle = game?.optJSONObject(\"battle-state\")"
-           "        if (battle == null) {"
-           "            titlePaint.textAlign = Paint.Align.CENTER"
-           "            canvas.drawText(\"SSD Damage\", drawWidth / 2f, drawHeight / 2f + 7f, titlePaint)"
-           "            return" "        }" "" "        val gap = 10f"
-           "        val top = 10f"
-           "        val cardWidth = (drawWidth - gap * 3f) / 2f"
-           "        drawShipSsd(canvas, RectF(gap, top, gap + cardWidth, drawHeight - 10f), \"ALPHA\", \"alpha\", battle.optJSONObject(\"alpha\"), alphaTruth)"
-           "        drawShipSsd(canvas, RectF(gap * 2f + cardWidth, top, gap * 2f + cardWidth * 2f, drawHeight - 10f), \"BETA\", \"beta\", battle.optJSONObject(\"beta\"), betaTruth)"
-           "    }" ""
-           "    private fun drawShipSsd(canvas: Canvas, rect: RectF, side: String, actor: String, ship: JSONObject?, truth: JSONObject?) {"
-           "        canvas.drawRoundRect(rect, 8f, 8f, panelPaint)"
-           "        canvas.drawRoundRect(rect, 8f, 8f, borderPaint)"
-           "        canvas.drawRoundRect(RectF(rect.left, rect.top, rect.right, rect.top + 27f), 8f, 8f, headerPaint)"
-           "        labelPaint.textAlign = Paint.Align.LEFT"
-           "        labelPaint.color = Color.WHITE"
-           "        canvas.drawText(side, rect.left + 8f, rect.top + 19f, labelPaint)"
-           "        smallPaint.textAlign = Paint.Align.RIGHT"
-           "        canvas.drawText(fitText(shipTechnicalLabel(ship, \"-\"), smallPaint, rect.width() * 0.46f), rect.right - 8f, rect.top + 19f, smallPaint)"
-           "" "        val name = shipDisplayLabel(ship)"
-           "        smallPaint.textAlign = Paint.Align.LEFT"
-           "        smallPaint.color = Color.rgb(187, 202, 223)"
-           "        canvas.drawText(fitText(name, smallPaint, rect.width() - 16f), rect.left + 8f, rect.top + 47f, smallPaint)"
-           "        val destroyed = counterMap(ship?.opt(\"destroyed-boxes\"))"
-           "        val recentHits = recentDamageTypes(actor)"
-           "        val destroyedTotal = destroyed.values.sum()"
-           "        val remaining = ship?.optInt(\"remaining-internals\", 0) ?: 0"
-           "        smallPaint.color = if (destroyedTotal > 0) Color.rgb(235, 169, 132) else Color.rgb(161, 177, 199)"
-           "        canvas.drawText(\"Internal damage $destroyedTotal   Hull $remaining\", rect.left + 8f, rect.top + 64f, smallPaint)"
-           "        smallPaint.color = Color.rgb(161, 177, 199)" ""
-           "        val defenseArea = RectF(rect.left + 8f, rect.top + 73f, rect.right - 8f, rect.top + 146f)"
-           "        drawDefenseBand(canvas, defenseArea, ship, destroyed)"
-           ""
-           "        val weaponHeight = if (rect.height() > 360f) 88f else 70f"
-           "        val groups = truth?.optJSONArray(\"groups\")"
-           "        if (groups != null && groups.length() > 0) {"
-           "            drawTruthGroups(" "                canvas,"
-           "                RectF(rect.left + 8f, defenseArea.bottom + 7f, rect.right - 8f, rect.bottom - weaponHeight - 30f),"
-           "                groups," "                destroyed,"
-           "                recentHits" "            )"
-           "            drawLegend(canvas, rect, destroyed, groupLabelMap(groups), recentHits)"
-           "        } else {" "            drawFallbackDamage("
-           "                canvas,"
-           "                RectF(rect.left + 8f, defenseArea.bottom + 9f, rect.right - 8f, rect.bottom - weaponHeight - 8f),"
-           "                destroyed," "                recentHits"
-           "            )" "        }"
-           "        drawWeaponStrip(canvas, RectF(rect.left + 8f, rect.bottom - weaponHeight - 8f, rect.right - 8f, rect.bottom - 8f), ship)"
-           "    }" ""
-           "    private fun drawTruthGroups(canvas: Canvas, area: RectF, groups: JSONArray, destroyed: Map<String, Int>, recentHits: Set<String>) {"
-           "        if (area.width() <= 12f || area.height() <= 12f) return"
-           "        val bounds = truthBounds(groups) ?: return"
-           "        val scale = min(area.width() / bounds.width(), area.height() / bounds.height())"
-           "        val drawWidth = bounds.width() * scale"
-           "        val drawHeight = bounds.height() * scale"
-           "        val left = area.left + (area.width() - drawWidth) / 2f"
-           "        val top = area.top + (area.height() - drawHeight) / 2f"
-           "        val remainingDestroyed = destroyed.toMutableMap()"
-           "" "        for (index in 0 until groups.length()) {"
-           "            val group = groups.optJSONObject(index) ?: continue"
-           "            val bbox = group.optJSONArray(\"bbox\") ?: continue"
-           "            val type = typeKey(group.opt(\"box-type-id\")) ?: continue"
-           "            val count = group.optInt(\"count\", group.optInt(\"cell-count\", 0)).coerceAtLeast(1)"
-           "            val groupRect = RectF("
-           "                left + ((bbox.optDouble(0).toFloat() - bounds.left) * scale),"
-           "                top + ((bbox.optDouble(1).toFloat() - bounds.top) * scale),"
-           "                left + ((bbox.optDouble(2).toFloat() - bounds.left) * scale),"
-           "                top + ((bbox.optDouble(3).toFloat() - bounds.top) * scale)"
-           "            )"
-           "            val lost = min(count, remainingDestroyed[type] ?: 0)"
-           "            if (lost > 0) remainingDestroyed[type] = (remainingDestroyed[type] ?: 0) - lost"
-           "            drawGroup(canvas, groupRect, type, group.optString(\"label\", type), count, lost)"
-           "            if (recentHits.contains(type)) {"
-           "                canvas.drawRoundRect(groupRect, 4f, 4f, recentHitPaint)"
-           "            }" "        }" "    }" ""
-           "    private fun drawGroup(canvas: Canvas, rect: RectF, type: String, label: String, count: Int, lost: Int) {"
-           "        val ratio = (lost.toFloat() / max(1, count).toFloat()).coerceIn(0f, 1f)"
-           "        val baseColor = colorForBoxType(type)"
-           "        boxPaint.color = when {"
-           "            ratio >= 1f -> Color.rgb(83, 61, 65)"
-           "            ratio > 0f -> dimColor(baseColor, 0.68f)"
-           "            else -> baseColor" "        }"
-           "        canvas.drawRoundRect(rect, 3f, 3f, boxPaint)"
-           "        if (ratio > 0f) {"
-           "            canvas.drawRoundRect(RectF(rect.left, rect.top, rect.left + rect.width() * ratio, rect.bottom), 3f, 3f, lostPaint)"
-           "        }"
-           "        canvas.drawRoundRect(rect, 3f, 3f, outlinePaint)"
-           "        if (rect.width() > 35f && rect.height() > 15f) {"
-           "            smallPaint.textAlign = Paint.Align.CENTER"
-           "            smallPaint.color = Color.WHITE"
-           "            val text = if (lost > 0) \"${shortLabel(label)} $lost/$count\" else shortLabel(label)"
-           "            canvas.drawText(fitText(text, smallPaint, rect.width() - 4f), rect.centerX(), rect.centerY() + 5f, smallPaint)"
-           "            smallPaint.color = Color.rgb(161, 177, 199)"
-           "        }" "    }" ""
-           "    private fun drawDefenseBand(canvas: Canvas, area: RectF, ship: JSONObject?, destroyed: Map<String, Int>) {"
-           "        if (area.height() < 45f || area.width() < 80f) return"
-           "        canvas.drawRoundRect(area, 6f, 6f, bandPaint)"
-           "        val shields = shieldValues(ship?.optJSONObject(\"shields\"))"
-           "        val maxShields = shieldValues(ship?.optJSONObject(\"max-shields\")).ifEmpty { shields }"
-           "        val currentTotal = shields.values.sum()"
-           "        val maxTotal = max(currentTotal, maxShields.values.sum())"
-           "        if (maxTotal > 0) {"
-           "            drawShieldGrid(canvas, area, shields, maxShields, currentTotal, maxTotal)"
-           "        } else {"
-           "            drawPaPanelStatus(canvas, area, ship, destroyed)"
-           "        }" "    }" "" "    private fun drawShieldGrid("
-           "        canvas: Canvas," "        area: RectF,"
-           "        shields: Map<Int, Int>,"
-           "        maxShields: Map<Int, Int>,"
-           "        currentTotal: Int," "        maxTotal: Int"
-           "    ) {" "        smallPaint.textAlign = Paint.Align.LEFT"
-           "        smallPaint.color = Color.rgb(190, 205, 224)"
-           "        canvas.drawText(\"SHIELDS $currentTotal/$maxTotal\", area.left + 6f, area.top + 14f, smallPaint)"
-           "" "        val colWidth = (area.width() - 12f) / 3f"
-           "        val cellHeight = min(22f, (area.height() - 23f) / 2f)"
-           "        val firstTop = area.top + 20f"
-           "        val layout = arrayOf("
-           "            ShieldSlot(6, 0, 0),"
-           "            ShieldSlot(1, 1, 0),"
-           "            ShieldSlot(2, 2, 0),"
-           "            ShieldSlot(5, 0, 1),"
-           "            ShieldSlot(4, 1, 1),"
-           "            ShieldSlot(3, 2, 1)" "        )"
-           "        layout.forEach { slot ->"
-           "            val left = area.left + 6f + slot.col * colWidth"
-           "            val top = firstTop + slot.row * (cellHeight + 4f)"
-           "            drawShieldCell(" "                canvas,"
-           "                RectF(left, top, left + colWidth - 4f, top + cellHeight),"
-           "                slot.number,"
-           "                shields[slot.number] ?: 0,"
-           "                maxShields[slot.number] ?: (shields[slot.number] ?: 0)"
-           "            )" "        }"
-           "        smallPaint.color = Color.rgb(161, 177, 199)"
-           "    }" ""
-           "    private fun drawShieldCell(canvas: Canvas, rect: RectF, number: Int, value: Int, maxValue: Int) {"
-           "        val safeMax = max(1, maxValue)"
-           "        val ratio = (value.toFloat() / safeMax.toFloat()).coerceIn(0f, 1f)"
-           "        canvas.drawRoundRect(rect, 3f, 3f, shieldTrackPaint)"
-           "        shieldValuePaint.color = shieldColor(ratio)"
-           "        canvas.drawRoundRect(RectF(rect.left, rect.top, rect.left + rect.width() * ratio, rect.bottom), 3f, 3f, shieldValuePaint)"
-           "        canvas.drawRoundRect(rect, 3f, 3f, outlinePaint)"
-           "        smallPaint.textAlign = Paint.Align.LEFT"
-           "        smallPaint.color = Color.WHITE"
-           "        canvas.drawText(\"$number\", rect.left + 4f, rect.bottom - 5f, smallPaint)"
-           "        smallPaint.textAlign = Paint.Align.RIGHT"
-           "        canvas.drawText(\"$value/$safeMax\", rect.right - 4f, rect.bottom - 5f, smallPaint)"
-           "    }" ""
-           "    private fun drawPaPanelStatus(canvas: Canvas, area: RectF, ship: JSONObject?, destroyed: Map<String, Int>) {"
-           "        val paPanels = counterValue(ship?.opt(\"boxes\"), \"52\")"
-           "        val paDestroyed = destroyed[\"52\"] ?: counterValue(ship?.opt(\"destroyed-boxes\"), \"52\")"
-           "        val paMax = max(1, paPanels + paDestroyed)"
-           "        val degradation = counterValue(ship?.opt(\"boxes\"), \"99\") + (destroyed[\"99\"] ?: 0)"
-           "        val ratio = (paPanels.toFloat() / paMax.toFloat()).coerceIn(0f, 1f)"
-           "        smallPaint.textAlign = Paint.Align.LEFT"
-           "        smallPaint.color = Color.rgb(190, 205, 224)"
-           "        canvas.drawText(\"PA PANELS $paPanels/$paMax\", area.left + 6f, area.top + 17f, smallPaint)"
-           "        smallPaint.textAlign = Paint.Align.RIGHT"
-           "        val detail = if (degradation > 0) \"DEG $degradation\" else \"NO SHIELDS\""
-           "        canvas.drawText(detail, area.right - 6f, area.top + 17f, smallPaint)"
-           "        val bar = RectF(area.left + 8f, area.top + 31f, area.right - 8f, area.top + 51f)"
-           "        canvas.drawRoundRect(bar, 5f, 5f, shieldTrackPaint)"
-           "        shieldValuePaint.color = Color.rgb(200, 139, 48)"
-           "        canvas.drawRoundRect(RectF(bar.left, bar.top, bar.left + bar.width() * ratio, bar.bottom), 5f, 5f, shieldValuePaint)"
-           "        canvas.drawRoundRect(bar, 5f, 5f, outlinePaint)"
-           "        smallPaint.textAlign = Paint.Align.LEFT"
-           "        smallPaint.color = Color.rgb(161, 177, 199)"
-           "        val note = if (paMax <= 1 && paPanels == 0) \"No shield/PA values in this template\" else \"Absorbs damage before internals\""
-           "        canvas.drawText(fitText(note, smallPaint, area.width() - 12f), area.left + 6f, min(area.bottom - 7f, bar.bottom + 16f), smallPaint)"
-           "    }" ""
-           "    private fun drawWeaponStrip(canvas: Canvas, area: RectF, ship: JSONObject?) {"
-           "        if (area.height() < 38f || area.width() < 90f) return"
-           "        canvas.drawRoundRect(area, 6f, 6f, bandPaint)"
-           "        smallPaint.textAlign = Paint.Align.LEFT"
-           "        smallPaint.color = Color.rgb(190, 205, 224)"
-           "        canvas.drawText(\"WEAPONS\", area.left + 6f, area.top + 15f, smallPaint)"
-           ""
-           "        val weapons = sortedWeaponStatus(ship?.optJSONArray(\"weapon-status\"))"
-           "        if (weapons.isEmpty()) {"
-           "            val fallback = compactCounter(counterMap(ship?.opt(\"weapons\")), 4)"
-           "            smallPaint.color = Color.rgb(161, 177, 199)"
-           "            canvas.drawText(fitText(if (fallback.isBlank()) \"No weapon status\" else fallback, smallPaint, area.width() - 12f), area.left + 6f, area.top + 35f, smallPaint)"
-           "            return" "        }" ""
-           "        val maxRows = max(1, ((area.height() - 22f) / 19f).toInt())"
-           "        weapons.take(maxRows).forEachIndexed { index, weapon ->"
-           "            val y = area.top + 34f + index * 19f"
-           "            drawWeaponRow(canvas, area, y, weapon)"
-           "        }" "        if (weapons.size > maxRows) {"
-           "            smallPaint.textAlign = Paint.Align.RIGHT"
-           "            smallPaint.color = Color.rgb(161, 177, 199)"
-           "            canvas.drawText(\"+${weapons.size - maxRows}\", area.right - 7f, area.bottom - 6f, smallPaint)"
-           "        }" "    }" ""
-           "    private fun drawWeaponRow(canvas: Canvas, area: RectF, y: Float, weapon: JSONObject) {"
-           "        val kind = shortWeapon(weapon.optString(\"kind\", \"-\"))"
-           "        val installed = weapon.optInt(\"installed\", 0).coerceAtLeast(0)"
-           "        val armed = weapon.optInt(\"armed\", 0).coerceAtLeast(0)"
-           "        val ready = weapon.optInt(\"fireable\", 0).coerceAtLeast(0)"
-           "        val inArc = weapon.optInt(\"fireable-in-arc\", 0).coerceAtLeast(0)"
-           "        val arcs = arcCodes(weapon)"
-           "        val arcSummary = compactArcSummary(arcs)" ""
-           "        smallPaint.textAlign = Paint.Align.LEFT"
-           "        smallPaint.color = Color.rgb(224, 233, 245)"
-           "        canvas.drawText(fitText(kind, smallPaint, area.width() * 0.28f), area.left + 6f, y, smallPaint)"
-           "        smallPaint.color = if (inArc > 0) Color.rgb(155, 223, 182) else Color.rgb(219, 174, 134)"
-           "        canvas.drawText(\"$ready/$armed/$installed\", area.left + area.width() * 0.30f, y, smallPaint)"
-           "        smallPaint.color = Color.rgb(203, 215, 231)"
-           "        canvas.drawText(fitText(arcSummary.ifBlank { \"-\" }, smallPaint, area.width() * 0.36f), area.left + area.width() * 0.48f, y, smallPaint)"
-           "" "        val chipCodes = arcs.distinct().take(3)"
-           "        var chipX = area.right - (chipCodes.size * 15f) - 2f"
-           "        chipCodes.forEach { code ->"
-           "            drawArcChip(canvas, chipX + 7f, y - 5f, 7f, code)"
-           "            chipX += 15f" "        }" "    }" ""
-           "    private fun drawLegend(canvas: Canvas, rect: RectF, destroyed: Map<String, Int>, labels: Map<String, String>, recentHits: Set<String>) {"
-           "        val text = if (destroyed.isEmpty()) {"
-           "            if (recentHits.isEmpty()) {"
-           "                \"No internal boxes destroyed\""
-           "            } else {"
-           "                \"Recent ${recentHits.take(3).joinToString(\" \") { shortLabel(labels[it] ?: it) }}\""
-           "            }" "        } else {"
-           "            val damageText = destroyed.entries"
-           "                .filter { it.value > 0 }"
-           "                .sortedByDescending { it.value }"
-           "                .take(4)"
-           "                .joinToString(\"  \") { \"${shortLabel(labels[it.key] ?: it.key)}:${it.value}\" }"
-           "            if (recentHits.isEmpty()) damageText else \"$damageText  recent ${recentHits.take(2).joinToString(\" \") { shortLabel(labels[it] ?: it) }}\""
-           "        }"
-           "        smallPaint.textAlign = Paint.Align.LEFT"
-           "        smallPaint.color = Color.rgb(187, 202, 223)"
-           "        canvas.drawText(fitText(text, smallPaint, rect.width() - 16f), rect.left + 8f, rect.bottom - 14f, smallPaint)"
-           "        smallPaint.color = Color.rgb(161, 177, 199)"
-           "    }" ""
-           "    private fun groupLabelMap(groups: JSONArray): Map<String, String> {"
-           "        val out = linkedMapOf<String, String>()"
-           "        for (index in 0 until groups.length()) {"
-           "            val group = groups.optJSONObject(index) ?: continue"
-           "            val type = typeKey(group.opt(\"box-type-id\")) ?: continue"
-           "            val label = group.optString(\"label\", \"\").trim()"
-           "            if (label.isNotBlank() && !out.containsKey(type)) out[type] = label"
-           "        }" "        return out" "    }" ""
-           "    private fun drawFallbackDamage(canvas: Canvas, area: RectF, destroyed: Map<String, Int>, recentHits: Set<String>) {"
-           "        if (area.width() <= 12f || area.height() <= 12f) return"
-           "        if (destroyed.isEmpty()) {"
-           "            labelPaint.textAlign = Paint.Align.CENTER"
-           "            labelPaint.color = Color.rgb(204, 216, 233)"
-           "            val text = if (recentHits.isEmpty()) \"No internal boxes destroyed\" else \"Recent ${recentHits.joinToString(\" \")}\""
-           "            canvas.drawText(fitText(text, labelPaint, area.width() - 8f), area.centerX(), area.centerY(), labelPaint)"
-           "            return" "        }"
-           "        smallPaint.textAlign = Paint.Align.LEFT"
-           "        smallPaint.color = Color.rgb(220, 230, 243)"
-           "        canvas.drawText(\"SSD truth not linked; showing damage counters\", area.left, area.top + 15f, smallPaint)"
-           "        val rows = destroyed.entries.filter { it.value > 0 }.sortedWith(compareByDescending<Map.Entry<String, Int>> { it.value }.thenBy { it.key })"
-           "        val maxRows = ((area.height() - 28f) / 22f).toInt().coerceAtLeast(1)"
-           "        rows.take(maxRows).forEachIndexed { index, entry ->"
-           "            val y = area.top + 42f + index * 22f"
-           "            canvas.drawCircle(area.left + 7f, y - 5f, 5f, lostPaint)"
-           "            canvas.drawText(\"type ${entry.key}: ${entry.value}\", area.left + 20f, y, smallPaint)"
-           "        }" "        if (rows.size > maxRows) {"
-           "            canvas.drawText(\"+${rows.size - maxRows} more\", area.left + 20f, area.bottom - 8f, smallPaint)"
-           "        }"
-           "        smallPaint.color = Color.rgb(161, 177, 199)"
-           "    }" ""
-           "    private fun truthBounds(groups: JSONArray): RectF? {"
-           "        var left = Float.POSITIVE_INFINITY"
-           "        var top = Float.POSITIVE_INFINITY"
-           "        var right = Float.NEGATIVE_INFINITY"
-           "        var bottom = Float.NEGATIVE_INFINITY"
-           "        for (index in 0 until groups.length()) {"
-           "            val bbox = groups.optJSONObject(index)?.optJSONArray(\"bbox\") ?: continue"
-           "            left = min(left, bbox.optDouble(0).toFloat())"
-           "            top = min(top, bbox.optDouble(1).toFloat())"
-           "            right = max(right, bbox.optDouble(2).toFloat())"
-           "            bottom = max(bottom, bbox.optDouble(3).toFloat())"
-           "        }"
-           "        if (!left.isFinite() || right <= left || bottom <= top) return null"
-           "        return RectF(left, top, right, bottom)" "    }" ""
-           "    private fun shieldValues(raw: JSONObject?): Map<Int, Int> {"
-           "        if (raw == null) return emptyMap()"
-           "        val out = linkedMapOf<Int, Int>()"
-           "        for (shield in 1..6) {"
-           "            val value = raw.optDouble(\"shield-$shield\", Double.NaN)"
-           "            if (!value.isNaN() && value > 0.0) out[shield] = value.roundToInt()"
-           "        }" "        return out" "    }" ""
-           "    private fun shieldColor(ratio: Float): Int ="
-           "        when {"
-           "            ratio >= 0.66f -> Color.rgb(78, 164, 210)"
-           "            ratio >= 0.33f -> Color.rgb(219, 172, 70)"
-           "            else -> Color.rgb(206, 82, 72)" "        }" ""
-           "    private fun colorForBoxType(type: String): Int ="
-           "        when (type.toIntOrNull()) {"
-           "            6, 12, 17, 189, 190, 191 -> Color.rgb(49, 89, 128)"
-           "            16, 20, 32, 58, 158, 159, 306, 307 -> Color.rgb(70, 95, 119)"
-           "            26, 27, 52, 99, 319 -> Color.rgb(57, 111, 122)"
-           "            28, 29, 30, 31, 33, 34, 35, 36, 37, 39, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 53, 54, 55, 56 -> Color.rgb(123, 83, 54)"
-           "            1, 2, 3, 4, 10, 21, 22, 23, 24, 59, 60, 101 -> Color.rgb(67, 103, 86)"
-           "            7, 8, 9, 25, 41, 57, 61, 69, 71 -> Color.rgb(82, 79, 110)"
-           "            else -> Color.rgb(53, 72, 96)" "        }" ""
-           "    private fun dimColor(color: Int, factor: Float): Int ="
-           "        Color.rgb("
-           "            (Color.red(color) * factor).roundToInt().coerceIn(0, 255),"
-           "            (Color.green(color) * factor).roundToInt().coerceIn(0, 255),"
-           "            (Color.blue(color) * factor).roundToInt().coerceIn(0, 255)"
-           "        )" ""
-           "    private fun counterValue(raw: Any?, wantedKey: String): Int ="
-           "        counterMap(raw)[wantedKey] ?: 0" ""
-           "    private fun compactCounter(values: Map<String, Int>, maxItems: Int): String ="
-           "        values.entries"
-           "            .filter { it.value > 0 }"
-           "            .sortedWith(compareByDescending<Map.Entry<String, Int>> { it.value }.thenBy { it.key })"
-           "            .take(maxItems)"
-           "            .joinToString(\" \") { \"${shortLabel(boxTypeName(it.key))}:${it.value}\" }"
-           ""
-           "    private fun sortedWeaponStatus(raw: JSONArray?): List<JSONObject> {"
-           "        if (raw == null) return emptyList()"
-           "        val out = ArrayList<JSONObject>()"
-           "        for (index in 0 until raw.length()) {"
-           "            raw.optJSONObject(index)?.let { out.add(it) }"
-           "        }" "        return out.sortedWith("
-           "            compareByDescending<JSONObject> { it.optInt(\"fireable-in-arc\", 0) }"
-           "                .thenByDescending { it.optInt(\"fireable\", 0) }"
-           "                .thenByDescending { it.optInt(\"armed\", 0) }"
-           "                .thenByDescending { it.optInt(\"installed\", 0) }"
-           "                .thenBy { it.optString(\"kind\", \"\") }"
-           "        )" "    }" ""
-           "    private fun shortWeapon(kind: String): String ="
-           "        kind.replace(\"phaser-\", \"PH-\")"
-           "            .replace(\"photon\", \"PHOT\")"
-           "            .replace(\"disruptor\", \"DISR\")"
-           "            .replace(\"plasma-\", \"PL-\")"
-           "            .replace(\"displacement-device\", \"DISP\")"
-           "            .replace(\"tractor-repulsor-heavy\", \"TRH\")"
-           "            .replace(\"tractor-repulsor-light\", \"TRL\")"
-           "            .uppercase()" ""
-           "    private fun arcCodes(weapon: JSONObject): List<String> {"
-           "        val raw = weapon.optJSONArray(\"arc-codes\") ?: JSONArray().put(weapon.optString(\"arc\", \"\"))"
-           "        val out = ArrayList<String>()"
-           "        for (index in 0 until raw.length()) {"
-           "            val value = raw.opt(index)?.toString()?.trim().orEmpty()"
-           "            if (value.isNotBlank()) out.add(value)"
-           "        }" "        return out" "    }" ""
-           "    private fun compactArcSummary(codes: List<String>): String {"
-           "        val counts = linkedMapOf<String, Int>()"
-           "        codes.forEach { code ->"
-           "            val label = arcCodeLabel(code)"
-           "            counts[label] = (counts[label] ?: 0) + 1"
-           "        }" "        return counts.entries"
-           "            .take(4)"
-           "            .joinToString(\" \") { if (it.value > 1) \"${it.key}x${it.value}\" else it.key }"
-           "    }" ""
-           "    private fun arcCodeLabel(raw: String): String {"
-           "        val numeric = raw.toIntOrNull()"
-           "        if (numeric == null) return raw.uppercase().ifBlank { \"-\" }"
-           "        val sectors = arcCodeSectors(numeric)"
-           "        val exact = when (sectors) {"
-           "            setOf(0, 1, 2, 3, 4, 5) -> \"360\""
-           "            setOf(5, 0, 1) -> \"FH\""
-           "            setOf(2, 3, 4) -> \"RH\""
-           "            setOf(0, 1, 2) -> \"LF\""
-           "            setOf(4, 5, 0) -> \"RF\""
-           "            setOf(1, 2, 3) -> \"L\""
-           "            setOf(3, 4, 5) -> \"R\""
-           "            setOf(2, 3) -> \"LR\""
-           "            setOf(3, 4) -> \"RR\""
-           "            setOf(0, 1) -> \"LF\""
-           "            setOf(5, 0) -> \"RF\""
-           "            setOf(1, 2) -> \"L\""
-           "            setOf(4, 5) -> \"R\""
-           "            else -> null" "        }"
-           "        if (exact != null) return exact"
-           "        val bits = arcBitLabels(numeric)"
-           "        return if (bits.isEmpty()) numeric.toString() else bits.joinToString(\"+\")"
-           "    }" ""
-           "    private fun drawArcChip(canvas: Canvas, cx: Float, cy: Float, radius: Float, raw: String) {"
-           "        val sectors = raw.toIntOrNull()?.let { arcCodeSectors(it) } ?: namedArcSectors(raw)"
-           "        val bounds = RectF(cx - radius, cy - radius, cx + radius, cy + radius)"
-           "        for (sector in 0..5) {"
-           "            val start = -30f + sector * 60f"
-           "            canvas.drawArc(bounds, start + 5f, 50f, true, if (sectors.contains(sector)) arcActivePaint else arcInactivePaint)"
-           "        }"
-           "        canvas.drawCircle(cx, cy, radius, arcStrokePaint)"
-           "    }" ""
-           "    private fun arcCodeSectors(code: Int): Set<Int> {"
-           "        if (code == 0 || code == 255 || code > 255) return setOf(0, 1, 2, 3, 4, 5)"
-           "        val out = linkedSetOf<Int>()"
-           "        fun add(bit: Int, sectors: IntArray) {"
-           "            if ((code and bit) != 0) sectors.forEach { out.add(it) }"
-           "        }" "        add(1, intArrayOf(5, 0))"
-           "        add(2, intArrayOf(4, 5))"
-           "        add(4, intArrayOf(3, 4))"
-           "        add(8, intArrayOf(2, 3))"
-           "        add(16, intArrayOf(1, 2))"
-           "        add(32, intArrayOf(0, 1))"
-           "        add(64, intArrayOf(5, 0, 1))"
-           "        add(128, intArrayOf(2, 3, 4))" "        return out"
-           "    }" ""
-           "    private fun arcBitLabels(code: Int): List<String> {"
-           "        if (code == 0 || code == 255 || code > 255) return listOf(\"360\")"
-           "        val out = ArrayList<String>()"
-           "        if ((code and 32) != 0) out.add(\"LF\")"
-           "        if ((code and 16) != 0) out.add(\"L\")"
-           "        if ((code and 8) != 0) out.add(\"LR\")"
-           "        if ((code and 4) != 0) out.add(\"RR\")"
-           "        if ((code and 2) != 0) out.add(\"R\")"
-           "        if ((code and 1) != 0) out.add(\"RF\")"
-           "        if ((code and 64) != 0) out.add(\"FH\")"
-           "        if ((code and 128) != 0) out.add(\"RH\")"
-           "        return out" "    }" ""
-           "    private fun namedArcSectors(raw: String): Set<Int> ="
-           "        when (raw.uppercase().replace(\" \", \"\")) {"
-           "            \"360\", \"ALL\", \"FX\" -> setOf(0, 1, 2, 3, 4, 5)"
-           "            \"FA\", \"F\", \"FH\" -> setOf(5, 0, 1)"
-           "            \"RH\" -> setOf(2, 3, 4)"
-           "            \"LF\" -> setOf(0, 1, 2)"
-           "            \"RF\" -> setOf(4, 5, 0)"
-           "            \"L\" -> setOf(1, 2, 3)"
-           "            \"R\" -> setOf(3, 4, 5)"
-           "            \"LS\", \"LR\" -> setOf(2, 3)"
-           "            \"RS\", \"RR\" -> setOf(3, 4)"
-           "            \"RA\", \"A\" -> setOf(2, 3, 4)"
-           "            else -> emptySet()" "        }" ""
-           "    private fun boxTypeName(type: String): String ="
-           "        when (type.toIntOrNull()) {"
-           "            1 -> \"Bridge\""
-           "            2 -> \"Flag Bridge\""
-           "            3 -> \"Scanner\""
-           "            4 -> \"Damage Control\""
-           "            5 -> \"Rear Hull\""
-           "            6 -> \"Left Warp\""
-           "            7 -> \"Transporter\""
-           "            8 -> \"Tractor\""
-           "            9 -> \"Shuttle\"" "            10 -> \"Lab\""
-           "            11 -> \"Forward Hull\""
-           "            12 -> \"Right Warp\""
-           "            13 -> \"Excess Damage\""
-           "            16 -> \"Impulse\""
-           "            17 -> \"Center Warp\""
-           "            18 -> \"Battery\"" "            20 -> \"APR\""
-           "            21 -> \"Probe\"" "            22 -> \"Sensor\""
-           "            23 -> \"Aux Control\""
-           "            24 -> \"Emergency Bridge\""
-           "            25 -> \"Cargo\"" "            26 -> \"Shield\""
-           "            27 -> \"Armor\"" "            28 -> \"Photon\""
-           "            29 -> \"Disruptor\""
-           "            30 -> \"TRH\"" "            31 -> \"TRL\""
-           "            32 -> \"AWR\"" "            33 -> \"Phaser-1\""
-           "            34 -> \"Phaser-2\""
-           "            35 -> \"Phaser-3\""
-           "            36 -> \"Phaser-4\""
-           "            37 -> \"Phaser-G\""
-           "            39 -> \"Drone-B\""
-           "            41 -> \"Security\""
-           "            42 -> \"Plasma-R\""
-           "            43 -> \"Plasma-S\""
-           "            44 -> \"Plasma-G\""
-           "            45 -> \"Plasma-F\""
-           "            46 -> \"Hellbore\""
-           "            47 -> \"Fusion\"" "            48 -> \"PPD\""
-           "            49 -> \"ESG\""
-           "            50 -> \"Web Caster\""
-           "            52 -> \"PA Panel\""
-           "            55 -> \"Option\""
-           "            56 -> \"Disp Dev\""
-           "            57 -> \"Center Hull\""
-           "            59 -> \"Repair\""
-           "            60 -> \"Mine Rack\""
-           "            61 -> \"Fighter\""
-           "            62 -> \"Drone-A\""
-           "            63 -> \"Drone-C\""
-           "            71 -> \"Hangar\""
-           "            99 -> \"PA Degradation\""
-           "            101 -> \"Special Sensor\""
-           "            else -> type" "        }" ""
-           "    private fun recentDamageTypes(actor: String): Set<String> {"
-           "        val events = game?.optJSONArray(\"events\") ?: return emptySet()"
-           "        for (index in events.length() - 1 downTo 0) {"
-           "            val event = events.optJSONObject(index) ?: continue"
-           "            val payload = event.optJSONObject(\"payload\") ?: continue"
-           "            val out = linkedSetOf<String>()"
-           "            if (event.optString(\"type\", \"\") == \"fire-weapons\") {"
-           "                addDamageEventTypes(out, payload.optJSONArray(\"$actor-damage-events\"))"
-           "                val summaries = payload.optJSONArray(\"fire-target-summaries\")"
-           "                if (summaries != null) {"
-           "                    for (summaryIndex in 0 until summaries.length()) {"
-           "                        val summary = summaries.optJSONObject(summaryIndex) ?: continue"
-           "                        if (summary.optString(\"target\", \"\") == actor) {"
-           "                            addDamageEventTypes(out, summary.optJSONArray(\"damage-events\"))"
-           "                        }" "                    }"
-           "                }"
-           "            } else if (event.optString(\"type\", \"\") == \"seeking-impact\" && payload.optString(\"target\", \"\") == actor) {"
-           "                addDamageEventTypes(out, payload.optJSONArray(\"damage-events\"))"
-           "            }"
-           "            if (out.isNotEmpty()) return out" "        }"
-           "        return emptySet()" "    }" ""
-           "    private fun addDamageEventTypes(out: MutableSet<String>, raw: JSONArray?) {"
-           "        if (raw == null) return"
-           "        for (index in 0 until raw.length()) {"
-           "            val event = raw.optJSONObject(index) ?: continue"
-           "            typeKey(event.opt(\"box-type\"))?.let { out.add(it) }"
-           "        }" "    }" ""
-           "    private fun counterMap(raw: Any?): Map<String, Int> {"
-           "        val out = linkedMapOf<String, Int>()"
-           "        when (raw) {" "            is JSONObject -> {"
-           "                val pair = raw.optJSONArray(PAIR_MARKER)"
-           "                if (pair != null) {"
-           "                    addCounterPair(out, pair.opt(0), pair.opt(1))"
-           "                } else {"
-           "                    val keys = raw.keys()"
-           "                    while (keys.hasNext()) {"
-           "                        val key = keys.next()"
-           "                        addCounterPair(out, key, raw.opt(key))"
-           "                    }" "                }" "            }"
-           "            is JSONArray -> {"
-           "                for (index in 0 until raw.length()) {"
-           "                    val item = raw.opt(index)"
-           "                    if (item is JSONObject) {"
-           "                        val pair = item.optJSONArray(PAIR_MARKER)"
-           "                        if (pair != null) addCounterPair(out, pair.opt(0), pair.opt(1))"
-           "                    }" "                }" "            }"
-           "        }" "        return out" "    }" ""
-           "    private fun addCounterPair(out: MutableMap<String, Int>, keyRaw: Any?, countRaw: Any?) {"
-           "        val key = typeKey(keyRaw) ?: return"
-           "        val count = numberValue(countRaw).coerceAtLeast(0)"
-           "        if (count > 0) out[key] = (out[key] ?: 0) + count"
-           "    }" "" "    private fun typeKey(value: Any?): String? ="
-           "        when (value) {"
-           "            is Number -> value.toInt().toString()"
-           "            is String -> value.toIntOrNull()?.toString() ?: value.takeIf { it.isNotBlank() }"
-           "            else -> null" "        }" ""
-           "    private fun numberValue(value: Any?): Int ="
-           "        when (value) {"
-           "            is Number -> value.toInt()"
-           "            is String -> value.toIntOrNull() ?: 0"
-           "            else -> 0" "        }" ""
-           "    private fun shortLabel(label: String): String ="
-           "        label.replace(\"phaser-\", \"ph-\")"
-           "            .replace(\"engine\", \"eng\")"
-           "            .replace(\"transporter\", \"tran\")"
-           "            .replace(\"tractor\", \"trac\")"
-           "            .replace(\"shield-\", \"sh\")"
-           "            .replace(\"shield\", \"sh\")"
-           "            .uppercase()" ""
-           "    private fun fitText(text: String, paint: Paint, maxWidth: Float): String {"
-           "        if (maxWidth <= 0f || 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...\""
-           "    }" "" "    companion object {"
-           "        private const val PAIR_MARKER = \"$\" + \"pair\""
-           "    }" ""
-           "    private data class ShieldSlot(val number: Int, val col: Int, val row: Int)"
-           "}"))
+        (include-fragment
+          "original-tactics-parts/SsdDamageView.kt.ss")
         (include-fragment
           "original-tactics-parts/SystemsReadinessView.kt.ss")
         (kotlin-file-lines