Integrate compiled typed Android client modules
ober
d168c109aad40c40e46d636b8eafcce3a2b6c1c1
--- a/templates/original-tactics.ss +++ b/templates/original-tactics.ss @@ -1,1052 +1,10 @@ (import (jerboa prelude)) (def fragment - '((kotlin-file-lines - "com/jerboa/originaltactics/BattleCommandView.kt" - ("package com.jerboa.originaltactics" "" - "import android.content.Context" - "import android.graphics.Canvas" - "import android.graphics.Color" - "import android.graphics.Paint" - "import android.graphics.RectF" "import android.view.View" - "import org.json.JSONArray" "import org.json.JSONObject" - "import kotlin.math.max" "import kotlin.math.min" "" - "class BattleCommandView(context: Context) : View(context) {" - " private var game: JSONObject? = null" - " private var events: JSONArray = JSONArray()" - " private var tacticalStatus: JSONObject? = null" - " private var drawWidth = 0f" - " private var drawHeight = 0f" "" - " private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(23, 29, 39)" - " style = Paint.Style.FILL" " }" - " private val panelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(34, 42, 55)" - " style = Paint.Style.FILL" " }" - " private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(83, 98, 122)" - " style = Paint.Style.STROKE" - " strokeWidth = 2f" " }" - " private val alphaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(47, 128, 222)" - " style = Paint.Style.FILL" " }" - " private val betaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(215, 92, 72)" - " style = Paint.Style.FILL" " }" - " private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(58, 68, 84)" - " style = Paint.Style.FILL" " }" - " private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(70, 167, 106)" - " style = Paint.Style.FILL" " }" - " private val accentPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(230, 155, 63)" - " style = Paint.Style.FILL" " }" - " private val titlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.WHITE" " textSize = 27f" - " isFakeBoldText = true" " }" - " private val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(213, 221, 234)" - " textSize = 20f" " }" - " private val smallPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(165, 177, 195)" - " textSize = 17f" " }" - " private val valuePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.WHITE" " textSize = 19f" - " isFakeBoldText = true" " }" "" - " fun setGame(nextGame: JSONObject?, nextEvents: JSONArray = JSONArray(), nextStatus: JSONObject? = tacticalStatus) {" - " game = nextGame" " events = nextEvents" - " tacticalStatus = nextStatus" " invalidate()" - " }" "" - " fun setTacticalStatus(nextStatus: JSONObject?) {" - " tacticalStatus = nextStatus" " invalidate()" - " }" "" " override fun onDraw(canvas: Canvas) {" - " super.onDraw(canvas)" - " val canvasState = canvas.save()" - " val scale = resources.displayMetrics.density.coerceAtLeast(1f)" - " drawWidth = width.toFloat() / scale" - " drawHeight = height.toFloat() / scale" - " canvas.scale(scale, scale)" " try {" - " drawContent(canvas)" " } finally {" - " canvas.restoreToCount(canvasState)" " }" - " }" "" " private fun drawContent(canvas: Canvas) {" - " canvas.drawRoundRect(RectF(0f, 0f, drawWidth, drawHeight), 10f, 10f, backgroundPaint)" - "" " val current = game" - " val battle = current?.optJSONObject(\"battle-state\")" - " if (current == null || battle == null) {" - " drawEmpty(canvas)" " return" - " }" "" " drawHeader(canvas, current, battle)" - " drawOrderStrip(canvas, current, battle)" - " val orders = current.optJSONObject(\"orders\")" - " val currentImpulse = current.optInt(\"impulse\", 0)" - "" " val gap = 10f" " val top = 82f" - " val feedHeight = min(54f, drawHeight * 0.32f)" - " val cardsBottom = max(top + 76f, drawHeight - feedHeight - 8f)" - " val cardWidth = (drawWidth - gap * 3f) / 2f" - " drawShipCard(" " canvas," - " RectF(gap, top, gap + cardWidth, cardsBottom)," - " \"ALPHA\"," - " battle.optJSONObject(\"alpha\")," - " battle.optJSONObject(\"alpha-position\")," - " tacticalStatus?.optJSONObject(\"alpha\")," - " orders," " \"alpha\"," - " currentImpulse," " alphaPaint" - " )" " drawShipCard(" " canvas," - " RectF(gap * 2f + cardWidth, top, gap * 2f + cardWidth * 2f, cardsBottom)," - " \"BETA\"," - " battle.optJSONObject(\"beta\")," - " battle.optJSONObject(\"beta-position\")," - " tacticalStatus?.optJSONObject(\"beta\")," - " orders," " \"beta\"," - " currentImpulse," " betaPaint" - " )" - " drawEventFeed(canvas, RectF(gap, cardsBottom + 6f, drawWidth - gap, drawHeight - 8f))" - " }" "" " private fun drawEmpty(canvas: Canvas) {" - " titlePaint.textAlign = Paint.Align.CENTER" - " canvas.drawText(\"Battle command\", drawWidth / 2f, drawHeight / 2f - 2f, titlePaint)" - " smallPaint.textAlign = Paint.Align.CENTER" - " canvas.drawText(\"Start an engagement\", drawWidth / 2f, drawHeight / 2f + 24f, smallPaint)" - " }" "" - " private fun drawHeader(canvas: Canvas, current: JSONObject, battle: JSONObject) {" - " val turn = current.optInt(\"turn\", 1)" - " val impulse = current.optInt(\"impulse\", 0)" - " val range = currentBattleRange(battle)" - " val winner = current.optString(\"winner\", \"undecided\")" - " val fired = current.optBoolean(\"fired?\", false)" - " val fireState = if (fired) \"FIRE RESOLVED\" else \"FIRE READY\"" - " val outcome = if (winner == \"undecided\") \"NO DECISION\" else \"${winner.uppercase()} WINS\"" - "" " titlePaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(\"T$turn.$impulse\", 12f, 29f, titlePaint)" - " valuePaint.textAlign = Paint.Align.CENTER" - " canvas.drawText(\"RANGE $range\", drawWidth / 2f, 29f, valuePaint)" - " val statusText = \"$fireState $outcome\"" - " valuePaint.textAlign = Paint.Align.RIGHT" - " canvas.drawText(fitText(statusText, valuePaint, drawWidth * 0.44f), drawWidth - 12f, 29f, valuePaint)" - " }" "" - " private fun drawOrderStrip(canvas: Canvas, current: JSONObject, battle: JSONObject) {" - " val orders = current.optJSONObject(\"orders\") ?: return" - " val currentImpulse = current.optInt(\"impulse\", 0)" - " val fireImpulse = orders.optInt(\"fire-impulse\", 32)" - " val alpha = maneuverPlotText(orders.opt(\"alpha-maneuvers\"), currentImpulse, 3)" - " val beta = maneuverPlotText(orders.opt(\"beta-maneuvers\"), currentImpulse, 3)" - " val ordersText = \"FIRE I$fireImpulse A $alpha B $beta\"" - " val forecastText = rangeForecastLine(battle, orders, currentImpulse)" - "" - " val rect = RectF(10f, 39f, drawWidth - 10f, 74f)" - " canvas.drawRoundRect(rect, 8f, 8f, panelPaint)" - " canvas.drawRoundRect(rect, 8f, 8f, borderPaint)" - " if (forecastText.isBlank()) {" - " valuePaint.textAlign = Paint.Align.CENTER" - " canvas.drawText(fitText(ordersText, valuePaint, rect.width() - 18f), rect.centerX(), rect.top + 24f, valuePaint)" - " } else {" - " valuePaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(fitText(ordersText, valuePaint, rect.width() - 18f), rect.left + 9f, rect.top + 16f, valuePaint)" - " smallPaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(fitText(forecastText, smallPaint, rect.width() - 18f), rect.left + 9f, rect.top + 32f, smallPaint)" - " }" " }" "" " private fun drawShipCard(" - " canvas: Canvas," " rect: RectF," - " side: String," " ship: JSONObject?," - " position: JSONObject?," - " statusActor: JSONObject?," - " orders: JSONObject?," " actor: String," - " currentImpulse: Int," " sidePaint: Paint" - " ) {" - " canvas.drawRoundRect(rect, 9f, 9f, panelPaint)" - " canvas.drawRoundRect(rect, 9f, 9f, borderPaint)" - " canvas.drawRoundRect(RectF(rect.left, rect.top, rect.right, rect.top + 28f), 9f, 9f, sidePaint)" - "" " valuePaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(side, rect.left + 9f, rect.top + 20f, valuePaint)" - "" " val name = shipDisplayLabel(ship)" - " labelPaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(fitText(name, labelPaint, rect.width() - 18f), rect.left + 9f, rect.top + 50f, labelPaint)" - "" " val speed = position?.optInt(\"speed\", 0) ?: 0" - " val facing = position?.optInt(\"facing\", 0) ?: 0" - " val helm = maneuverPlotText(orders?.opt(\"$actor-maneuvers\"), currentImpulse, 3)" - " val nextMove = nextMoveImpulse(position, currentImpulse)?.let { \"M I$it\" } ?: \"HOLD\"" - " smallPaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(fitText(\"SPD $speed F$facing $nextMove HELM $helm\", smallPaint, rect.width() - 18f), rect.left + 9f, rect.top + 70f, smallPaint)" - "" - " val internalNow = ship?.optInt(\"remaining-internals\", 0) ?: 0" - " val internalMax = maxInternals(ship, internalNow)" - " drawMetricBar(canvas, rect.left + 9f, rect.top + 84f, rect.width() - 18f, \"HULL\", internalNow, internalMax)" - "" " val shieldNow = shieldTotal(ship, false)" - " val shieldMax = max(shieldTotal(ship, true), shieldNow)" - " val paPanels = counterValue(ship?.opt(\"boxes\"), \"52\")" - " val paMax = paPanelMax(ship)" - " if (shieldMax <= 0 && (paPanels > 0 || paMax > 1)) {" - " drawMetricBar(canvas, rect.left + 9f, rect.top + 116f, rect.width() - 18f, \"PA PANELS\", paPanels, paMax)" - " } else {" - " drawMetricBar(canvas, rect.left + 9f, rect.top + 116f, rect.width() - 18f, \"SHIELDS\", shieldNow, shieldMax)" - " }" "" - " val weaponTotal = counterTotal(ship?.optJSONObject(\"weapons\"))" - " val armedTotal = counterTotal(ship?.optJSONObject(\"armed-weapons\"))" - " drawMetricBar(canvas, rect.left + 9f, rect.top + 148f, rect.width() - 18f, \"ARMED\", armedTotal, max(1, weaponTotal))" - "" - " val fireSolution = actorFireSolution(statusActor)" - " val fireLine = when {" - " fireSolution.installed > 0 -> \"LOCK ${fireSolution.inArc}/${fireSolution.ready} READY ${fireSolution.ready}/${fireSolution.armed}\"" - " else -> \"LOCK - READY -\"" " }" - " smallPaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(fitText(fireLine, smallPaint, rect.width() - 18f), rect.left + 9f, min(rect.bottom - 30f, rect.top + 180f), smallPaint)" - "" - " val cool = counterTotal(ship?.optJSONObject(\"direct-fire-cooldown\"))" - " val launch = counterTotal(ship?.optJSONObject(\"seeking-launch-limits\"))" - " val families = weaponFamilyLine(statusActor)" - " val primary = primaryWeaponLine(statusActor)" - " val footer = when {" - " families.isNotBlank() -> \"$families COOL $cool\"" - " primary.isNotBlank() -> \"$primary COOL $cool\"" - " else -> \"COOL $cool LAUNCH $launch\"" - " }" - " canvas.drawText(fitText(footer, smallPaint, rect.width() - 18f), rect.left + 9f, min(rect.bottom - 10f, rect.top + 201f), smallPaint)" - " }" "" - " private fun drawMetricBar(canvas: Canvas, x: Float, y: Float, barWidth: Float, label: String, value: Int, maxValue: Int) {" - " val safeMax = max(1, maxValue)" - " val ratio = (value.toFloat() / safeMax.toFloat()).coerceIn(0f, 1f)" - " labelPaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(label, x, y + 13f, smallPaint)" - " valuePaint.textAlign = Paint.Align.RIGHT" - " canvas.drawText(\"$value/$safeMax\", x + barWidth, y + 13f, valuePaint)" - "" " val top = y + 18f" - " val rect = RectF(x, top, x + barWidth, top + 9f)" - " canvas.drawRoundRect(rect, 5f, 5f, trackPaint)" - " if (ratio > 0f) {" - " fillPaint.color = barColor(ratio)" - " canvas.drawRoundRect(RectF(rect.left, rect.top, rect.left + rect.width() * ratio, rect.bottom), 5f, 5f, fillPaint)" - " }" " }" "" - " private fun drawEventFeed(canvas: Canvas, rect: RectF) {" - " canvas.drawRoundRect(rect, 8f, 8f, panelPaint)" - " canvas.drawRoundRect(rect, 8f, 8f, borderPaint)" - " smallPaint.textAlign = Paint.Align.LEFT" - " smallPaint.color = Color.rgb(165, 177, 195)" - " canvas.drawText(\"TACTICAL LOG\", rect.left + 9f, rect.top + 18f, smallPaint)" - "" " val lines = recentEventLines(3)" - " if (lines.isEmpty()) {" - " labelPaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(\"No combat events yet\", rect.left + 9f, rect.top + 42f, labelPaint)" - " } else {" " var y = rect.top + 40f" - " lines.forEachIndexed { index, line ->" - " if (y > rect.bottom - 6f) return@forEachIndexed" - " if (index == lines.lastIndex) {" - " canvas.drawCircle(rect.left + 10f, y - 5f, 4f, accentPaint)" - " }" - " labelPaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(fitText(line, labelPaint, rect.width() - 28f), rect.left + 20f, y, labelPaint)" - " y += 19f" " }" " }" - " smallPaint.color = Color.rgb(165, 177, 195)" - " }" "" - " private fun recentEventLines(limit: Int): List<String> {" - " val out = mutableListOf<String>()" - " for (index in events.length() - 1 downTo 0) {" - " val event = events.optJSONObject(index) ?: continue" - " val type = event.optString(\"type\", \"\")" - " if (type.isBlank() || type == \"impulse-activity\") continue" - " out.add(0, summarizeEvent(event))" - " if (out.size >= limit) break" " }" - " return out" " }" "" - " private fun summarizeEvent(event: JSONObject): String {" - " val turn = event.optInt(\"turn\", -1)" - " val impulse = event.optInt(\"impulse\", -1)" - " val actor = event.optString(\"actor\", \"\")" - " val type = event.optString(\"type\", \"event\")" - " val payload = event.optJSONObject(\"payload\")" - " val prefix = when {" - " turn >= 0 && impulse >= 0 -> \"T$turn.$impulse\"" - " impulse >= 0 -> \"I$impulse\"" - " else -> \"LOG\"" " }" - " val actorText = if (actor.isBlank()) \"\" else \" ${actor.uppercase()}\"" - " val detail = when (type) {" - " \"fire-weapons\" -> fireSummary(payload)" - " \"seeking-launch\" -> launchSummary(payload)" - " \"seeking-impact\" -> impactSummary(payload)" - " \"movement\" -> movementSummary(payload)" - " \"turn\", \"sideslip\", \"high-energy-turn\" -> movementSummary(payload)" - " \"turn-blocked\", \"maneuver-blocked\", \"fire-control-blocked\" -> blockedSummary(payload)" - " \"ew-control\", \"ew-support\", \"ew-support-rejected\", \"ew-support-lost\"," - " \"lock-on-control\", \"shield-control\", \"energy-allocation\", \"wild-swac\" -> controlSummary(payload)" - " \"web-created\", \"web-reduced\", \"web-caster\", \"web-snare\", \"web-breaker\" -> webSummary(payload)" - " \"turn-completed\" -> \"turn complete\"" - " \"orders-submitted\" -> \"orders committed\"" - " else -> payload?.optString(\"reason\", type)?.takeIf { it.isNotBlank() } ?: type" - " }" " return \"$prefix$actorText $detail\"" - " }" "" - " private fun fireSummary(payload: JSONObject?): String {" - " if (payload == null) return \"weapons fire\"" - " val summaries = payload.optJSONArray(\"fire-target-summaries\")" - " if (summaries != null && summaries.length() > 0) {" - " val parts = mutableListOf<String>()" - " for (index in 0 until summaries.length()) {" - " val summary = summaries.optJSONObject(index) ?: continue" - " val attacker = sideLabel(summary.optString(\"attacker\", \"\"))" - " val target = sideLabel(summary.optString(\"target\", \"\"))" - " val damage = numberValue(summary.opt(\"damage\"))" - " val internals = numberValue(summary.opt(\"internals\"))" - " val face = shieldFaceLabel(summary.opt(\"shield-facing\"))" - " val before = numberValue(summary.opt(\"shield-before\"))" - " val after = numberValue(summary.opt(\"shield-after\"))" - " if (damage <= 0 && internals <= 0) continue" - " val shield = if (face.isBlank()) \"\" else \" $face $before>$after\"" - " val internalText = if (internals > 0) \" int $internals\" else \"\"" - " parts.add(\"$attacker>$target dmg $damage$shield$internalText\")" - " if (parts.size >= 2) break" " }" - " if (parts.isNotEmpty()) return parts.joinToString(\" \")" - " }" - " val alphaDamage = payload.optJSONObject(\"alpha-volley\")?.optInt(\"damage\", 0) ?: 0" - " val betaDamage = payload.optJSONObject(\"beta-volley\")?.optInt(\"damage\", 0) ?: 0" - " val range = payload.opt(\"range-before\") ?: payload.opt(\"range\") ?: \"-\"" - " return \"weapons fire R$range A:$alphaDamage B:$betaDamage\"" - " }" "" - " private fun launchSummary(payload: JSONObject?): String {" - " val seeker = payload?.optJSONObject(\"seeker\") ?: return \"seeker launch\"" - " val kind = seeker.optString(\"kind\", \"seeker\")" - " val id = seeker.opt(\"id\") ?: seeker.opt(\"seeking-id\") ?: \"?\"" - " val target = seeker.optString(\"target\", \"?\")" - " return \"launch $kind #$id to $target\"" " }" "" - " private fun impactSummary(payload: JSONObject?): String {" - " if (payload == null) return \"seeker impact\"" - " val kind = payload.optString(\"kind\", payload.optString(\"seeker-kind\", \"seeker\"))" - " val target = payload.optString(\"target\", \"?\")" - " val damage = payload.opt(\"damage\") ?: \"-\"" - " val face = shieldFaceLabel(payload.opt(\"shield-facing\"))" - " val before = numberValue(payload.opt(\"shield-before\"))" - " val after = numberValue(payload.opt(\"shield-after\"))" - " val shield = if (face.isBlank()) \"\" else \" $face $before>$after\"" - " return \"$kind impact $damage on $target$shield\"" - " }" "" - " private fun movementSummary(payload: JSONObject?): String {" - " val after = payload?.optJSONObject(\"position-after\") ?: return \"maneuver\"" - " return \"move q${after.optInt(\"q\")} r${after.optInt(\"r\")} f${after.optInt(\"facing\")}\"" - " }" "" - " private fun blockedSummary(payload: JSONObject?): String {" - " if (payload == null) return \"blocked\"" - " val maneuver = payload.optString(\"maneuver\", \"\").uppercase()" - " val reason = payload.optString(\"reason\", \"blocked\").replace('-', ' ')" - " return listOf(maneuver, reason).filter { it.isNotBlank() }.joinToString(\" \")" - " }" "" - " private fun controlSummary(payload: JSONObject?): String =" - " payloadFieldSummary(" " payload," - " listOf(\"mode\", \"reason\", \"ecm\", \"eccm\", \"ew-shift\", \"power\", \"reserve-power\", \"fire-control-active?\", \"shield\", \"target\")" - " ).ifBlank { \"control\" }" "" - " private fun webSummary(payload: JSONObject?): String =" - " payloadFieldSummary(payload, listOf(\"strength\", \"web-strength\", \"web-hexes\", \"hexes\", \"damage\", \"range\", \"reason\"))" - " .ifBlank { \"web action\" }" "" - " private fun payloadFieldSummary(payload: JSONObject?, keys: List<String>): String {" - " if (payload == null) return \"\"" - " val parts = mutableListOf<String>()" - " keys.forEach { key ->" - " if (!payload.has(key)) return@forEach" - " val value = payload.opt(key)" - " if (value == null || value == JSONObject.NULL) return@forEach" - " parts.add(\"${fieldLabel(key)} ${compactPayloadValue(value)}\")" - " }" - " return parts.take(3).joinToString(\" \")" " }" - "" " private fun fieldLabel(key: String): String =" - " key.removeSuffix(\"?\")" - " .replace(\"-active\", \"\")" - " .replace(\"shield-facing\", \"shield\")" - " .replace('-', ' ')" " .uppercase()" - "" - " private fun compactPayloadValue(value: Any?): String =" - " when (value) {" " is JSONObject -> {" - " val q = value.opt(\"q\")" - " val r = value.opt(\"r\")" - " val facing = value.opt(\"facing\")" - " if (q != null && r != null) \"q$q/r$r/f${facing ?: \"-\"}\" else \"{${value.length()}}\"" - " }" - " is JSONArray -> value.length().toString()" - " is Boolean -> if (value) \"Y\" else \"N\"" - " else -> value.toString().replace('-', ' ')" - " }" "" - " private fun maneuverPlotText(raw: Any?, currentImpulse: Int, limit: Int): String {" - " val entries = maneuverEntries(raw)" - " .filter { it.first > currentImpulse && it.second.isNotBlank() }" - " .sortedBy { it.first }" - " .take(limit)" - " if (entries.isEmpty()) return \"CLEAR\"" - " return entries.joinToString(\"/\") { (impulse, maneuver) ->" - " \"I$impulse ${maneuverAbbrev(maneuver)}\"" - " }" " }" "" - " private fun rangeForecastLine(battle: JSONObject, orders: JSONObject, currentImpulse: Int): String {" - " val forecast = projectedRangeForecast(" - " battle.optJSONObject(\"alpha-position\")," - " battle.optJSONObject(\"beta-position\")," - " orders.opt(\"alpha-maneuvers\")," - " orders.opt(\"beta-maneuvers\")," - " currentImpulse," " maxMoves = 8," - " labelLimit = 5" " )" - " if (forecast.isEmpty()) return \"\"" - " val currentRange = hexRangeFromPositions(" - " battle.optJSONObject(\"alpha-position\")," - " battle.optJSONObject(\"beta-position\")" - " )" " val finalRange = forecast.last().range" - " val trend = when {" - " currentRange == null -> \"FORECAST\"" - " finalRange < currentRange -> \"CLOSING\"" - " finalRange > currentRange -> \"OPENING\"" - " else -> \"RANGE\"" " }" - " val points = forecast.take(4).joinToString(\" \") { \"I${it.impulse} R${it.range}\" }" - " return \"$trend $points\"" " }" "" - " private fun maneuverEntries(raw: Any?): List<Pair<Int, String>> {" - " val entries = mutableListOf<Pair<Int, String>>()" - " when (raw) {" " is JSONArray -> {" - " for (index in 0 until raw.length()) {" - " val item = raw.opt(index)" - " when (item) {" - " is JSONObject -> {" - " val pair = item.optJSONArray(\"\\$pair\")" - " if (pair != null) {" - " val impulse = pair.optInt(0, -1)" - " val maneuver = pair.optString(1, \"\")" - " if (impulse > 0 && maneuver.isNotBlank()) entries.add(impulse to maneuver)" - " }" " }" - " is JSONArray -> {" - " val impulse = item.optInt(0, -1)" - " val maneuver = item.optString(1, \"\")" - " if (impulse > 0 && maneuver.isNotBlank()) entries.add(impulse to maneuver)" - " }" " }" - " }" " }" - " is JSONObject -> {" - " val keys = raw.keys()" - " while (keys.hasNext()) {" - " val key = keys.next()" - " val impulse = key.toIntOrNull() ?: continue" - " val maneuver = raw.optString(key, \"\")" - " if (impulse > 0 && maneuver.isNotBlank()) entries.add(impulse to maneuver)" - " }" " }" " }" - " return entries" " }" "" - " private fun maneuverAbbrev(maneuver: String): String =" - " maneuverAbbreviation(maneuver)" "" - " private fun maxInternals(ship: JSONObject?, current: Int): Int {" - " val explicit = ship?.optInt(\"max-internals\", 0) ?: 0" - " if (explicit > 0) return explicit" - " return max(1, current + counterTotal(ship?.optJSONObject(\"destroyed-boxes\")))" - " }" "" - " private fun shieldTotal(ship: JSONObject?, maxValues: Boolean): Int {" - " val preferred = if (maxValues) ship?.optJSONObject(\"max-shields\") else ship?.optJSONObject(\"shields\")" - " val fallback = ship?.optJSONObject(\"shields\")" - " return counterTotal(preferred ?: fallback)" " }" - "" " private fun paPanelMax(ship: JSONObject?): Int {" - " val current = counterValue(ship?.opt(\"boxes\"), \"52\")" - " val destroyed = counterValue(ship?.opt(\"destroyed-boxes\"), \"52\")" - " return max(1, current + destroyed)" " }" "" - " private fun counterTotal(counter: Any?): Int {" - " var total = 0" - " forEachCounterPair(counter) { _, value ->" - " total += numberValue(value).coerceAtLeast(0)" - " }" " return total" " }" "" - " private fun counterValue(counter: Any?, wantedKey: String): Int {" - " var total = 0" - " forEachCounterPair(counter) { key, value ->" - " if (key == wantedKey) total += numberValue(value).coerceAtLeast(0)" - " }" " return total" " }" "" - " private fun forEachCounterPair(counter: Any?, action: (String, Any?) -> Unit) {" - " when (counter) {" " is JSONObject -> {" - " val pair = counter.optJSONArray(PAIR_MARKER)" - " if (pair != null) {" - " action(pair.opt(0)?.toString() ?: \"\", pair.opt(1))" - " } else {" - " val keys = counter.keys()" - " while (keys.hasNext()) {" - " val key = keys.next()" - " action(key, counter.opt(key))" - " }" " }" " }" - " is JSONArray -> {" - " for (index in 0 until counter.length()) {" - " val pair = counter.optJSONObject(index)?.optJSONArray(PAIR_MARKER) ?: continue" - " action(pair.opt(0)?.toString() ?: \"\", pair.opt(1))" - " }" " }" " }" " }" "" - " private fun numberValue(value: Any?): Int =" - " when (value) {" - " is Number -> value.toInt()" - " is String -> value.toIntOrNull() ?: 0" - " else -> 0" " }" "" - " private fun sideLabel(value: String): String =" - " when (value.lowercase()) {" - " \"alpha\" -> \"A\"" - " \"beta\" -> \"B\"" - " else -> value.uppercase().take(6)" " }" - "" " private fun shieldFaceLabel(value: Any?): String {" - " val text = value?.toString() ?: return \"\"" - " if (text == \"#f\" || text == \"false\") return \"\"" - " if (text == \"all\") return \"ALL\"" - " val number = text.removePrefix(\"shield-\").toIntOrNull() ?: return text.uppercase()" - " return \"SH$number\"" " }" "" - " private fun actorFireSolution(actor: JSONObject?): FireSolution {" - " val weapons = actor?.optJSONArray(\"weapon-status\") ?: return FireSolution()" - " var installed = 0" " var armed = 0" - " var ready = 0" " var inArc = 0" - " for (index in 0 until weapons.length()) {" - " val weapon = weapons.optJSONObject(index) ?: continue" - " installed += weapon.optInt(\"installed\", 0).coerceAtLeast(0)" - " armed += weapon.optInt(\"armed\", 0).coerceAtLeast(0)" - " ready += weapon.optInt(\"fireable\", 0).coerceAtLeast(0)" - " inArc += weapon.optInt(\"fireable-in-arc\", 0).coerceAtLeast(0)" - " }" - " return FireSolution(installed, armed, ready, inArc)" - " }" "" - " private fun primaryWeaponLine(actor: JSONObject?): String {" - " val weapons = actor?.optJSONArray(\"weapon-status\") ?: return \"\"" - " var best: JSONObject? = null" - " for (index in 0 until weapons.length()) {" - " val weapon = weapons.optJSONObject(index) ?: continue" - " val current = best" - " if (current == null || weaponScore(weapon) > weaponScore(current)) {" - " best = weapon" " }" " }" - " val weapon = best ?: return \"\"" - " val kind = shortWeapon(weapon.optString(\"kind\", \"-\"))" - " val arc = compactArray(weapon.optJSONArray(\"arc-codes\"), 2)" - " return \"$kind ${weapon.optInt(\"fireable-in-arc\", 0)}/${weapon.optInt(\"armed\", 0)}${if (arc.isBlank()) \"\" else \" $arc\"}\"" - " }" "" - " private fun weaponFamilyLine(actor: JSONObject?): String {" - " val weapons = actor?.optJSONArray(\"weapon-status\") ?: return \"\"" - " val entries = mutableListOf<JSONObject>()" - " for (index in 0 until weapons.length()) {" - " val weapon = weapons.optJSONObject(index) ?: continue" - " val installed = weapon.optInt(\"installed\", 0).coerceAtLeast(0)" - " val armed = weapon.optInt(\"armed\", 0).coerceAtLeast(0)" - " val fireable = weapon.optInt(\"fireable\", 0).coerceAtLeast(0)" - " val cooldown = weapon.optInt(\"cooldown\", 0).coerceAtLeast(0)" - " if (installed + armed + fireable + cooldown > 0) entries.add(weapon)" - " }" " return entries" - " .sortedWith(compareByDescending<JSONObject> { weaponScore(it) }.thenBy { it.optString(\"kind\", \"\") })" - " .take(3)" - " .joinToString(\" \") { weapon ->" - " val kind = shortWeapon(weapon.optString(\"kind\", \"-\"))" - " val fireable = weapon.optInt(\"fireable-in-arc\", weapon.optInt(\"fireable\", 0)).coerceAtLeast(0)" - " val armed = weapon.optInt(\"armed\", 0).coerceAtLeast(0)" - " val installed = weapon.optInt(\"installed\", 0).coerceAtLeast(0)" - " val cooldown = weapon.optInt(\"cooldown\", 0).coerceAtLeast(0)" - " val cooldownText = if (cooldown > 0) \" cd$cooldown\" else \"\"" - " \"$kind $fireable/$armed/$installed$cooldownText\"" - " }" " }" "" - " private fun weaponScore(weapon: JSONObject): Int =" - " weapon.optInt(\"fireable-in-arc\", 0) * 1000 +" - " weapon.optInt(\"fireable\", 0) * 100 +" - " weapon.optInt(\"armed\", 0) * 10 +" - " weapon.optInt(\"installed\", 0)" "" - " private fun shortWeapon(kind: String): String =" - " kind.replace(\"phaser-\", \"PH-\")" - " .replace(\"photon\", \"PHOT\")" - " .replace(\"disruptor\", \"DISR\")" - " .replace(\"plasma-\", \"PL-\")" - " .replace(\"drone-\", \"DR-\")" - " .uppercase()" "" - " private fun compactArray(array: JSONArray?, limit: Int): String {" - " if (array == null || array.length() == 0) return \"\"" - " val out = mutableListOf<String>()" - " for (index in 0 until minOf(array.length(), limit)) {" - " out.add(array.opt(index).toString())" - " }" - " if (array.length() > limit) out.add(\"+${array.length() - limit}\")" - " return out.joinToString(\"/\")" " }" "" - " private fun barColor(ratio: Float): Int =" - " when {" - " ratio > 0.66f -> Color.rgb(70, 167, 106)" - " ratio > 0.33f -> Color.rgb(230, 155, 63)" - " else -> Color.rgb(215, 92, 72)" " }" "" - " private fun fitText(text: String, paint: Paint, maxWidth: Float): String {" - " if (paint.measureText(text) <= maxWidth) return text" - " var candidate = text" - " while (candidate.length > 4 && paint.measureText(\"$candidate...\") > maxWidth) {" - " candidate = candidate.dropLast(1)" " }" - " return if (candidate.length <= 4) candidate else \"$candidate...\"" - " }" "" " private data class FireSolution(" - " val installed: Int = 0," - " val armed: Int = 0," " val ready: Int = 0," - " val inArc: Int = 0" " )" "" - " companion object {" - " private const val PAIR_MARKER = \"$\" + \"pair\"" - " }" "}")) - (kotlin-file-lines - "com/jerboa/originaltactics/BridgeHudView.kt" - ("package com.jerboa.originaltactics" "" - "import android.content.Context" - "import android.graphics.Canvas" - "import android.graphics.Color" - "import android.graphics.Paint" - "import android.graphics.RectF" "import android.view.View" - "import org.json.JSONArray" "import org.json.JSONObject" - "import kotlin.math.max" "" - "class BridgeHudView(context: Context) : View(context) {" - " private var game: JSONObject? = null" - " private var dashboard: JSONObject? = null" - " private var events: JSONArray = JSONArray()" - " private var drawWidth = 0f" - " private var drawHeight = 0f" "" - " private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(8, 13, 21)" - " style = Paint.Style.FILL" " }" - " private val panelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(18, 27, 40)" - " style = Paint.Style.FILL" " }" - " private val panelAltPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(23, 34, 49)" - " style = Paint.Style.FILL" " }" - " private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(80, 102, 132)" - " style = Paint.Style.STROKE" - " strokeWidth = 2f" " }" - " private val alphaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(54, 139, 232)" - " style = Paint.Style.FILL" " }" - " private val betaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(217, 84, 69)" - " style = Paint.Style.FILL" " }" - " private val readyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(76, 181, 114)" - " style = Paint.Style.FILL" " }" - " private val warningPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(224, 161, 69)" - " style = Paint.Style.FILL" " }" - " private val dangerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(191, 68, 62)" - " style = Paint.Style.FILL" " }" - " private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(50, 63, 83)" - " style = Paint.Style.FILL" " }" - " private val titlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.WHITE" " textSize = 25f" - " isFakeBoldText = true" " }" - " private val valuePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.WHITE" " textSize = 20f" - " isFakeBoldText = true" " }" - " private val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(202, 214, 232)" - " textSize = 17f" " }" - " private val smallPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(145, 162, 188)" - " textSize = 15f" " }" - " private val shieldTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.WHITE" " textSize = 12f" - " isFakeBoldText = true" - " textAlign = Paint.Align.CENTER" " }" - " private val shieldHitPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(255, 229, 120)" - " style = Paint.Style.STROKE" - " strokeWidth = 2.4f" " }" "" - " fun setData(nextGame: JSONObject?, nextDashboard: JSONObject?, nextEvents: JSONArray = JSONArray()) {" - " game = nextGame" - " dashboard = nextDashboard" - " events = nextEvents" " invalidate()" " }" - "" " override fun onDraw(canvas: Canvas) {" - " super.onDraw(canvas)" - " val canvasState = canvas.save()" - " val scale = resources.displayMetrics.density.coerceAtLeast(1f)" - " drawWidth = width.toFloat() / scale" - " drawHeight = height.toFloat() / scale" - " canvas.scale(scale, scale)" " try {" - " drawContent(canvas)" " } finally {" - " canvas.restoreToCount(canvasState)" " }" - " }" "" " private fun drawContent(canvas: Canvas) {" - " canvas.drawRect(0f, 0f, drawWidth, drawHeight, backgroundPaint)" - " val current = game" - " val battle = current?.optJSONObject(\"battle-state\")" - " if (current == null || battle == null) {" - " drawEmpty(canvas)" " return" - " }" "" - " val status = dashboard?.optJSONObject(\"status\")" - " drawHeader(canvas, current, battle, status)" "" - " val gap = 9f" " val top = 38f" - " val bottom = drawHeight - 8f" - " if (drawWidth < 520f) {" - " val shipBottom = top + (bottom - top) * 0.62f" - " val cardWidth = (drawWidth - gap * 3f) / 2f" - " drawShipBlock(" " canvas," - " RectF(gap, top, gap + cardWidth, shipBottom)," - " \"ALPHA\"," - " battle.optJSONObject(\"alpha\")," - " status?.optJSONObject(\"alpha\")," - " shieldFacingIndex(battle.opt(\"alpha-shield-facing-hit\"))," - " alphaPaint" " )" - " drawShipBlock(" " canvas," - " RectF(gap * 2f + cardWidth, top, gap * 2f + cardWidth * 2f, shipBottom)," - " \"BETA\"," - " battle.optJSONObject(\"beta\")," - " status?.optJSONObject(\"beta\")," - " shieldFacingIndex(battle.opt(\"beta-shield-facing-hit\"))," - " betaPaint" " )" - " drawCenterBlock(canvas, RectF(gap, shipBottom + gap, drawWidth - gap, bottom), current, battle, status)" - " } else {" - " val leftWidth = drawWidth * 0.36f" - " val rightWidth = drawWidth * 0.36f" - " val centerLeft = gap + leftWidth + gap" - " val centerRight = drawWidth - gap - rightWidth - gap" - " drawShipBlock(" " canvas," - " RectF(gap, top, gap + leftWidth, bottom)," - " \"ALPHA\"," - " battle.optJSONObject(\"alpha\")," - " status?.optJSONObject(\"alpha\")," - " shieldFacingIndex(battle.opt(\"alpha-shield-facing-hit\"))," - " alphaPaint" " )" - " drawCenterBlock(canvas, RectF(centerLeft, top, centerRight, bottom), current, battle, status)" - " drawShipBlock(" " canvas," - " RectF(drawWidth - gap - rightWidth, top, drawWidth - gap, bottom)," - " \"BETA\"," - " battle.optJSONObject(\"beta\")," - " status?.optJSONObject(\"beta\")," - " shieldFacingIndex(battle.opt(\"beta-shield-facing-hit\"))," - " betaPaint" " )" " }" - " }" "" " private fun drawEmpty(canvas: Canvas) {" - " titlePaint.textAlign = Paint.Align.CENTER" - " canvas.drawText(\"TACTICAL COMMAND\", drawWidth / 2f, drawHeight / 2f - 5f, titlePaint)" - " smallPaint.textAlign = Paint.Align.CENTER" - " canvas.drawText(\"No active engagement\", drawWidth / 2f, drawHeight / 2f + 21f, smallPaint)" - " }" "" - " private fun drawHeader(canvas: Canvas, current: JSONObject, battle: JSONObject, status: JSONObject?) {" - " val turn = current.optInt(\"turn\", status?.optInt(\"turn\", 0) ?: 0)" - " val impulse = current.optInt(\"impulse\", status?.optInt(\"impulse\", 0) ?: 0)" - " val range = currentBattleRange(battle).ifBlank { (status?.opt(\"range\") ?: \"-\").toString() }" - " val winner = current.optString(\"winner\", status?.optString(\"winner\", \"undecided\") ?: \"undecided\")" - " val outcome = if (winner == \"undecided\") \"NO DECISION\" else \"${winner.uppercase()} WINS\"" - "" " titlePaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(\"TACTICAL COMMAND\", 10f, 26f, titlePaint)" - " valuePaint.textAlign = Paint.Align.CENTER" - " canvas.drawText(\"T$turn.$impulse RANGE $range\", drawWidth / 2f, 25f, valuePaint)" - " valuePaint.textAlign = Paint.Align.RIGHT" - " canvas.drawText(fitText(outcome, valuePaint, drawWidth * 0.34f), drawWidth - 10f, 25f, valuePaint)" - " }" "" " private fun drawShipBlock(" - " canvas: Canvas," " rect: RectF," - " side: String," " ship: JSONObject?," - " statusShip: JSONObject?," " hitShield: Int," - " sidePaint: Paint" " ) {" - " canvas.drawRoundRect(rect, 8f, 8f, panelPaint)" - " canvas.drawRoundRect(rect, 8f, 8f, borderPaint)" - " canvas.drawRoundRect(RectF(rect.left, rect.top, rect.right, rect.top + 25f), 8f, 8f, sidePaint)" - "" " valuePaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(side, rect.left + 8f, rect.top + 18f, valuePaint)" - " smallPaint.textAlign = Paint.Align.RIGHT" - " canvas.drawText(fitText(shipId(ship, statusShip), smallPaint, rect.width() * 0.46f), rect.right - 8f, rect.top + 17f, smallPaint)" - "" " var y = rect.top + 45f" - " labelPaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(fitText(shipName(ship, statusShip), labelPaint, rect.width() - 16f), rect.left + 8f, y, labelPaint)" - " y += 19f" "" - " val movement = statusShip?.optJSONObject(\"movement\")" - " val power = statusShip?.optJSONObject(\"power-status\")" - " val speed = movement?.opt(\"current-speed\") ?: power?.opt(\"planned-speed\") ?: \"-\"" - " val planned = movement?.opt(\"planned-speed\") ?: \"-\"" - " val powerText = \"SPD $speed/$planned PWR ${power?.optInt(\"unspent-energy\", 0) ?: 0}/${power?.optInt(\"total-power\", 0) ?: 0}\"" - " smallPaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(fitText(powerText, smallPaint, rect.width() - 16f), rect.left + 8f, y, smallPaint)" - " y += 20f" "" - " val hullNow = ship?.optInt(\"remaining-internals\", statusShip?.optInt(\"remaining-internals\", 0) ?: 0) ?: 0" - " val hullMax = max(hullNow, counterTotal(ship?.opt(\"boxes\")))" - " if (rect.height() < 118f) {" - " val armed = counterTotal(ship?.optJSONObject(\"armed-weapons\"))" - " smallPaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(fitText(\"INT $hullNow ${defenseSummary(ship, statusShip)} WPN $armed\", smallPaint, rect.width() - 16f), rect.left + 8f, y, smallPaint)" - " return" " }" - " drawBar(canvas, rect.left + 8f, y, rect.width() - 16f, \"HULL\", hullNow, max(1, hullMax))" - " y += 29f" "" - " drawShieldStrip(canvas, rect.left + 8f, y, rect.width() - 16f, ship, statusShip, hitShield)" - " y += 29f" "" " val weaponTotal = max(" - " counterTotal(ship?.optJSONObject(\"weapons\"))," - " weaponInstalledTotal(statusShip?.optJSONArray(\"weapon-status\"))" - " )" - " val armed = counterTotal(ship?.optJSONObject(\"armed-weapons\"))" - " drawBar(canvas, rect.left + 8f, y, rect.width() - 16f, \"ARMED\", armed, max(1, weaponTotal))" - " y += 29f" "" - " val weaponText = weaponFamilySummary(statusShip?.optJSONArray(\"weapon-status\"))" - " if (weaponText.isNotBlank() && y <= rect.bottom - 8f) {" - " smallPaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(fitText(\"WPN $weaponText\", smallPaint, rect.width() - 16f), rect.left + 8f, y + 12f, smallPaint)" - " }" " }" "" - " private fun drawCenterBlock(canvas: Canvas, rect: RectF, current: JSONObject, battle: JSONObject, status: JSONObject?) {" - " canvas.drawRoundRect(rect, 8f, 8f, panelAltPaint)" - " canvas.drawRoundRect(rect, 8f, 8f, borderPaint)" "" - " val compact = rect.height() < 72f" - " val orders = current.optJSONObject(\"orders\")" - " val fireImpulse = orders?.opt(\"fire-impulse\") ?: \"-\"" - " val fireText = if (current.optBoolean(\"fired?\", false)) \"FIRE RESOLVED\" else \"FIRE I$fireImpulse\"" - " val phaseText = phaseSlotSummary(dashboard?.optJSONObject(\"impulse-activity\")?.optJSONArray(\"stage-slots\"))" - " valuePaint.textAlign = Paint.Align.CENTER" - " canvas.drawText(fitText(fireText, valuePaint, rect.width() - 12f), rect.centerX(), rect.top + if (compact) 18f else 24f, valuePaint)" - "" " val counts = status?.optJSONObject(\"counts\")" - " val seekers = counts?.optInt(\"active-seeking\", countUnits(battle.opt(\"seeking-weapons\"))) ?: countUnits(battle.opt(\"seeking-weapons\"))" - " val smallUnits = counts?.optInt(\"active-small-units\", countUnits(battle.opt(\"shuttles\"))) ?: countUnits(battle.opt(\"shuttles\"))" - " val webs = counts?.optInt(\"active-webs\", 0) ?: 0" - " val mines = counts?.optInt(\"active-mines\", 0) ?: 0" - " val hidden = counts?.optInt(\"detected-hidden-units\", 0) ?: 0" - "" " var y = rect.top + if (compact) 38f else 49f" - " if (!compact && phaseText.isNotBlank()) {" - " smallPaint.textAlign = Paint.Align.CENTER" - " canvas.drawText(fitText(phaseText, smallPaint, rect.width() - 14f), rect.centerX(), y, smallPaint)" - " y += 18f" " }" " if (!compact) {" - " val commandText = \"CMD A ${availableActionCount(\"alpha\")}/${totalActionCount(\"alpha\")} B ${availableActionCount(\"beta\")}/${totalActionCount(\"beta\")}\"" - " smallPaint.textAlign = Paint.Align.CENTER" - " canvas.drawText(fitText(commandText, smallPaint, rect.width() - 14f), rect.centerX(), y, smallPaint)" - " y += 18f" " }" " if (compact) {" - " val countText = \"SEEK $seekers UNITS $smallUnits WEB $webs MINES $mines HIDDEN $hidden\"" - " labelPaint.textAlign = Paint.Align.CENTER" - " canvas.drawText(fitText(countText, labelPaint, rect.width() - 14f), rect.centerX(), y, labelPaint)" - " if (rect.height() < 58f) return" - " y += 18f" " } else {" - " y = drawCountPill(canvas, rect, y, \"SEEK\", seekers, if (seekers > 0) warningPaint else readyPaint)" - " y = drawCountPill(canvas, rect, y, \"UNITS\", smallUnits, if (smallUnits > 0) warningPaint else readyPaint)" - " y = drawCountPill(canvas, rect, y, \"WEB\", webs, if (webs > 0) warningPaint else readyPaint)" - " y = drawCountPill(canvas, rect, y, \"MINES\", mines, if (mines > 0) dangerPaint else readyPaint)" - " y = drawCountPill(canvas, rect, y, \"HIDDEN\", hidden, if (hidden > 0) dangerPaint else readyPaint)" - " }" "" - " val eventText = latestEventText(status?.optJSONArray(\"recent-events\") ?: events)" - " smallPaint.textAlign = Paint.Align.CENTER" - " val eventY = if (compact) rect.bottom - 10f else minOf(rect.bottom - 12f, y + 10f)" - " canvas.drawText(fitText(eventText, smallPaint, rect.width() - 14f), rect.centerX(), eventY, smallPaint)" - " }" "" - " private fun drawCountPill(canvas: Canvas, rect: RectF, y: Float, label: String, value: Int, paint: Paint): Float {" - " val pill = RectF(rect.left + 8f, y - 14f, rect.right - 8f, y + 8f)" - " canvas.drawRoundRect(pill, 8f, 8f, trackPaint)" - " val accent = RectF(pill.left, pill.top, pill.left + 5f, pill.bottom)" - " canvas.drawRoundRect(accent, 8f, 8f, paint)" - " labelPaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(label, pill.left + 11f, y + 2f, labelPaint)" - " valuePaint.textAlign = Paint.Align.RIGHT" - " canvas.drawText(value.toString(), pill.right - 8f, y + 2f, valuePaint)" - " return y + 26f" " }" "" - " private fun drawBar(canvas: Canvas, x: Float, y: Float, width: Float, label: String, value: Int, maxValue: Int) {" - " val safeMax = max(1, maxValue)" - " val ratio = (value.toFloat() / safeMax.toFloat()).coerceIn(0f, 1f)" - " smallPaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(label, x, y + 12f, smallPaint)" - " valuePaint.textAlign = Paint.Align.RIGHT" - " canvas.drawText(\"$value/$safeMax\", x + width, y + 12f, valuePaint)" - " val bar = RectF(x, y + 16f, x + width, y + 25f)" - " canvas.drawRoundRect(bar, 5f, 5f, trackPaint)" - " val fillPaint = when {" - " ratio > 0.55f -> readyPaint" - " ratio > 0.25f -> warningPaint" - " else -> dangerPaint" " }" - " canvas.drawRoundRect(RectF(bar.left, bar.top, bar.left + bar.width() * ratio, bar.bottom), 5f, 5f, fillPaint)" - " }" "" " private fun drawShieldStrip(" - " canvas: Canvas," " x: Float," - " y: Float," " width: Float," - " ship: JSONObject?," - " statusShip: JSONObject?," " hitShield: Int" - " ) {" - " val shields = ship?.optJSONObject(\"shields\") ?: statusShip?.optJSONObject(\"shields\")" - " val maxShields = ship?.optJSONObject(\"max-shields\") ?: statusShip?.optJSONObject(\"max-shields\")" - " val totalNow = objectTotal(shields)" - " val totalMax = max(totalNow, objectTotal(maxShields))" - " smallPaint.textAlign = Paint.Align.LEFT" - " valuePaint.textAlign = Paint.Align.RIGHT" "" - " if (totalMax <= 0) {" - " val paPanels = paPanelCount(ship, statusShip)" - " val paMax = paPanelMax(ship, statusShip)" - " val ratio = (paPanels.toFloat() / paMax.toFloat()).coerceIn(0f, 1f)" - " canvas.drawText(\"DEFENSE\", x, y + 12f, smallPaint)" - " canvas.drawText(if (paPanels > 0 || paMax > 1) \"PA $paPanels/$paMax\" else \"NO SHIELDS\", x + width, y + 12f, valuePaint)" - " val bar = RectF(x, y + 16f, x + width, y + 25f)" - " canvas.drawRoundRect(bar, 5f, 5f, trackPaint)" - " if (ratio > 0f) {" - " canvas.drawRoundRect(RectF(bar.left, bar.top, bar.left + bar.width() * ratio, bar.bottom), 5f, 5f, warningPaint)" - " shieldTextPaint.textSize = 11f" - " canvas.drawText(\"PA PANELS\", bar.centerX(), y + 24f, shieldTextPaint)" - " }" " return" " }" "" - " canvas.drawText(\"SHIELDS\", x, y + 12f, smallPaint)" - " canvas.drawText(\"$totalNow/$totalMax\", x + width, y + 12f, valuePaint)" - "" " val gap = 2f" - " val cellWidth = (width - gap * 5f) / 6f" - " val top = y + 16f" " val bottom = y + 25f" - " shieldTextPaint.textSize = if (cellWidth >= 34f) 11f else 9f" - " for (index in 0 until 6) {" - " val shieldNumber = index + 1" - " val key = \"shield-$shieldNumber\"" - " val now = numberValue(shields?.opt(key)).coerceAtLeast(0)" - " val maxValue = max(now, numberValue(maxShields?.opt(key)).coerceAtLeast(0))" - " val ratio = if (maxValue > 0) (now.toFloat() / maxValue.toFloat()).coerceIn(0f, 1f) else 0f" - " val left = x + index * (cellWidth + gap)" - " val cell = RectF(left, top, left + cellWidth, bottom)" - " canvas.drawRoundRect(cell, 3f, 3f, trackPaint)" - " if (maxValue > 0 && ratio > 0f) {" - " val fillPaint = when {" - " ratio > 0.55f -> readyPaint" - " ratio > 0.25f -> warningPaint" - " else -> dangerPaint" - " }" - " canvas.drawRoundRect(RectF(cell.left, cell.top, cell.left + cell.width() * ratio, cell.bottom), 3f, 3f, fillPaint)" - " }" - " if (hitShield == shieldNumber) {" - " canvas.drawRoundRect(RectF(cell.left - 1f, cell.top - 2f, cell.right + 1f, cell.bottom + 2f), 4f, 4f, shieldHitPaint)" - " }" " if (cellWidth >= 24f) {" - " val label = if (cellWidth >= 38f) \"$shieldNumber:$now\" else shieldNumber.toString()" - " canvas.drawText(label, cell.centerX(), bottom - 1f, shieldTextPaint)" - " }" " }" " }" "" - " 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\", \"\") }" - " .replace(\"Launch \", \"Ln \")" - " .replace(\"Fire Weapons\", \"Fire\")" - " .replace(\"Announce \", \"\")" - " if (action.isNotBlank() && !names.contains(action)) names.add(action)" - " if (names.size >= 3) break" " }" - " return names.joinToString(\" | \")" " }" "" - " private fun availableActionCount(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(actor: String): Int =" - " dashboard?.optJSONObject(\"actions\")?.optJSONArray(actor)?.length() ?: 0" - "" - " private fun shipName(ship: JSONObject?, statusShip: JSONObject?): String {" - " val fromShip = shipDisplayLabel(ship, \"\")" - " if (fromShip.isNotBlank()) return fromShip" - " return shipDisplayLabel(statusShip)" " }" "" - " private fun shipId(ship: JSONObject?, statusShip: JSONObject?): String {" - " val fromShip = shipTechnicalLabel(ship, \"\")" - " if (fromShip.isNotBlank()) return fromShip" - " return shipTechnicalLabel(statusShip)" " }" "" - " private fun counterTotal(counter: Any?): Int {" - " var total = 0" - " forEachCounterPair(counter) { _, value ->" - " total += numberValue(value).coerceAtLeast(0)" - " }" " return total" " }" "" - " private fun objectTotal(obj: JSONObject?): Int {" - " var total = 0" - " forEachCounterPair(obj) { _, value ->" - " total += numberValue(value).coerceAtLeast(0)" - " }" " return total" " }" "" - " private fun paPanelCount(ship: JSONObject?, statusShip: JSONObject?): Int =" - " counterValue(ship?.opt(\"boxes\"), \"52\").takeIf { it > 0 }" - " ?: counterValue(statusShip?.opt(\"boxes\"), \"52\")" - "" - " private fun paPanelMax(ship: JSONObject?, statusShip: JSONObject?): Int {" - " val current = paPanelCount(ship, statusShip)" - " val destroyed = counterValue(ship?.opt(\"destroyed-boxes\"), \"52\").takeIf { it > 0 }" - " ?: counterValue(statusShip?.opt(\"destroyed-boxes\"), \"52\")" - " return max(1, current + destroyed)" " }" "" - " private fun counterValue(counter: Any?, wantedKey: String): Int {" - " var total = 0" - " forEachCounterPair(counter) { key, value ->" - " if (key == wantedKey) total += numberValue(value).coerceAtLeast(0)" - " }" " return total" " }" "" - " private fun forEachCounterPair(counter: Any?, action: (String, Any?) -> Unit) {" - " when (counter) {" " is JSONObject -> {" - " val pair = counter.optJSONArray(PAIR_MARKER)" - " if (pair != null) {" - " action(pair.opt(0)?.toString() ?: \"\", pair.opt(1))" - " } else {" - " val keys = counter.keys()" - " while (keys.hasNext()) {" - " val key = keys.next()" - " action(key, counter.opt(key))" - " }" " }" " }" - " is JSONArray -> {" - " for (index in 0 until counter.length()) {" - " val pair = counter.optJSONObject(index)?.optJSONArray(PAIR_MARKER) ?: continue" - " action(pair.opt(0)?.toString() ?: \"\", pair.opt(1))" - " }" " }" " }" " }" "" - " private fun defenseSummary(ship: JSONObject?, statusShip: JSONObject?): String {" - " val shieldNow = objectTotal(ship?.optJSONObject(\"shields\") ?: statusShip?.optJSONObject(\"shields\"))" - " if (shieldNow > 0) return \"SH $shieldNow\"" - " val paPanels = paPanelCount(ship, statusShip)" - " val paMax = paPanelMax(ship, statusShip)" - " return if (paPanels > 0 || paMax > 1) \"PA $paPanels/$paMax\" else \"SH 0\"" - " }" "" - " private fun shieldFacingIndex(value: Any?): Int =" - " when (value) {" - " is Number -> value.toInt()" - " is String -> {" - " val digits = value.removePrefix(\"shield-\")" - " digits.toIntOrNull() ?: -1" " }" - " else -> -1" - " }.takeIf { it in 1..6 } ?: -1" "" - " 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 weaponInstalledTotal(weapons: JSONArray?): Int {" - " if (weapons == null) return 0" - " var total = 0" - " for (index in 0 until weapons.length()) {" - " total += weapons.optJSONObject(index)?.optInt(\"installed\", 0)?.coerceAtLeast(0) ?: 0" - " }" " return total" " }" "" - " private fun weaponFamilySummary(weapons: JSONArray?): String {" - " if (weapons == null) return \"\"" - " val entries = mutableListOf<WeaponFamily>()" - " for (index in 0 until weapons.length()) {" - " val weapon = weapons.optJSONObject(index) ?: continue" - " val installed = weapon.optInt(\"installed\", 0).coerceAtLeast(0)" - " val armed = weapon.optInt(\"armed\", 0).coerceAtLeast(0)" - " val fireable = weapon.optInt(\"fireable\", 0).coerceAtLeast(0)" - " val inArc = weapon.optInt(\"fireable-in-arc\", 0).coerceAtLeast(0)" - " val cooldown = weapon.optInt(\"cooldown\", 0).coerceAtLeast(0)" - " if (installed + armed + fireable + cooldown <= 0) continue" - " entries.add(WeaponFamily(weapon.optString(\"kind\", \"\"), installed, armed, fireable, inArc, cooldown))" - " }" " return entries" - " .sortedWith(compareBy<WeaponFamily>({ weaponSortBucket(it.kind) }, { it.kind }))" - " .take(3)" - " .joinToString(\" \") { entry ->" - " val arc = if (entry.fireable > 0 && entry.inArc < entry.fireable) \" a${entry.inArc}\" else \"\"" - " val cooldown = if (entry.cooldown > 0) \" cd${entry.cooldown}\" else \"\"" - " \"${shortWeaponKind(entry.kind)} ${entry.fireable}/${entry.armed}/${entry.installed}$arc$cooldown\"" - " }" " }" "" - " private fun shortWeaponKind(kind: String): String =" - " when (kind) {" " \"photon\" -> \"PHT\"" - " \"disruptor\" -> \"DIS\""