Use typed SSD damage view
ober
cf4dc931bd48399856225a4c704362be0c045da4
--- 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